"""Copy uploaded prompt media into the git workspace for repo-based agents."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
import os
import re
import shutil
from typing import Any, Optional
from uuid import UUID

import structlog

from src.config import get_settings

logger = structlog.get_logger()


@dataclass(frozen=True)
class PromptMediaActionsBundle:
    """Prompt media files prepared for a GitHub Actions checkout."""

    prompt: str
    files: dict[str, bytes]
    upload_dir: Optional[str]
    cloud_refs: list[dict[str, Any]]


def _safe_filename(name: str) -> str:
    base = os.path.basename(name or "") or "upload.bin"
    base = re.sub(r"[^a-zA-Z0-9._-]+", "_", base).strip("._")[:180]
    return base or "upload.bin"


async def augment_prompt_with_media(
    *,
    db_factory,
    project_id: UUID,
    run_id: UUID,
    repo_path: Optional[str],
    base_user_prompt: str,
    plan_context: dict[str, Any],
) -> str:
    """Load assets from DB; optionally copy into ``repo_path``; return prompt with appendix."""
    ids_raw = plan_context.get("attachment_ids") or []
    voice = plan_context.get("voice_transcript")
    if isinstance(voice, str):
        voice = voice.strip() or None
    else:
        voice = None

    asset_ids: list[UUID] = []
    for x in ids_raw:
        try:
            asset_ids.append(UUID(str(x)))
        except (ValueError, TypeError):
            continue

    if not asset_ids and not voice:
        return base_user_prompt

    from src.domain.prompt_media.compose import build_multimodal_prompt_appendix
    from src.domain.prompt_media.repository import PromptMediaRepository

    async with db_factory() as db:
        repo = PromptMediaRepository(db)
        assets = await repo.list_by_ids_for_project(asset_ids, project_id)

    if asset_ids and len(assets) != len(set(asset_ids)):
        logger.warning(
            "prompt_media_missing_assets",
            project_id=str(project_id),
            run_id=str(run_id),
            expected=len(asset_ids),
            found=len(assets),
        )

    settings = get_settings()
    root = os.path.normpath(settings.prompt_media_upload_root)

    copied: list[tuple[str, str]] = []
    if repo_path and assets:
        sub = os.path.join("p2hs", "prompt-media", str(run_id))
        dest_dir = os.path.join(repo_path, sub)
        os.makedirs(dest_dir, exist_ok=True)
        for i, a in enumerate(assets):
            src = os.path.join(root, a.storage_relative_path.replace("/", os.sep))
            if not os.path.isfile(src):
                logger.error("prompt_media_source_missing", path=src, asset_id=str(a.id))
                continue
            dst_name = f"{i}_{_safe_filename(a.original_filename)}"
            dst = os.path.join(dest_dir, dst_name)
            try:
                await asyncio.to_thread(shutil.copy2, src, dst)
                copied.append((dst_name, a.kind))
            except OSError as e:
                logger.error("prompt_media_copy_failed", error=str(e), src=src, dst=dst)

    repo_subdir = f"p2hs/prompt-media/{run_id}" if copied else None

    if copied:
        appendix = build_multimodal_prompt_appendix(
            voice_transcript=voice,
            copied_files=copied,
            repo_subdir=repo_subdir,
        )
    else:
        loose = [(a.original_filename, a.kind) for a in assets] if assets else None
        appendix = build_multimodal_prompt_appendix(
            voice_transcript=voice,
            unattached_descriptions=loose,
        )

    appendix = appendix.strip()
    if not appendix:
        return base_user_prompt
    base = (base_user_prompt or "").strip()
    if not base:
        return appendix
    return base + "\n\n" + appendix


async def prepare_prompt_media_for_actions(
    *,
    db_factory,
    project_id: UUID,
    run_id: UUID,
    base_user_prompt: str,
    plan_context: dict[str, Any],
    upload_dir_prefix: str = ".p2hs-user-uploads",
) -> PromptMediaActionsBundle:
    """Load prompt media and return GitHub-commit-ready files plus an augmented prompt."""
    ids_raw = plan_context.get("attachment_ids") or []
    voice = plan_context.get("voice_transcript")
    if isinstance(voice, str):
        voice = voice.strip() or None
    else:
        voice = None

    asset_ids: list[UUID] = []
    for x in ids_raw:
        try:
            asset_ids.append(UUID(str(x)))
        except (ValueError, TypeError):
            continue

    if not asset_ids and not voice:
        return PromptMediaActionsBundle(
            prompt=base_user_prompt,
            files={},
            upload_dir=None,
            cloud_refs=[],
        )

    from src.core.exceptions import ConflictError
    from src.domain.project_storage.repository import (
        ProjectStorageBucketRepository,
        ProjectStorageUsageRepository,
    )
    from src.domain.project_storage.schemas import CreateBucketRequest, UpdateBucketRequest
    from src.domain.project_storage.service import ProjectStorageService
    from src.domain.prompt_media.compose import build_multimodal_prompt_appendix
    from src.domain.prompt_media.repository import PromptMediaRepository
    from src.domain.projects.repository import ProjectRepository
    from src.integrations.cloud_control_client import get_cloud_client_for_workspace

    async with db_factory() as db:
        repo = PromptMediaRepository(db)
        assets = await repo.list_by_ids_for_project(asset_ids, project_id)
        project = await ProjectRepository(db).get_by_id(project_id)
        workspace_id = project.workspace_id if project else None

    if asset_ids and len(assets) != len(set(asset_ids)):
        logger.warning(
            "prompt_media_missing_assets_for_actions",
            project_id=str(project_id),
            run_id=str(run_id),
            expected=len(asset_ids),
            found=len(assets),
        )

    settings = get_settings()
    root = os.path.normpath(settings.prompt_media_upload_root)
    upload_dir = f"{upload_dir_prefix}/{run_id}"
    files: dict[str, bytes] = {}
    copied: list[tuple[str, str]] = []
    cloud_refs: list[dict[str, Any]] = []
    cloud_client = None
    bucket_name = "prompt-media"

    if workspace_id and assets:
        async with db_factory() as db:
            cloud_client = await get_cloud_client_for_workspace(workspace_id, db)
            storage_service = ProjectStorageService(
                bucket_repo=ProjectStorageBucketRepository(db),
                usage_repo=ProjectStorageUsageRepository(db),
                cloud_client=cloud_client,
            )
            try:
                await storage_service.create_bucket(
                    project_id,
                    CreateBucketRequest(
                        name=bucket_name,
                        access_policy="private",
                        rls_enabled=False,
                    ),
                )
                await db.commit()
            except ConflictError:
                await db.rollback()
                await storage_service.update_bucket(
                    project_id,
                    bucket_name,
                    UpdateBucketRequest(
                        access_policy="private",
                        rls_enabled=False,
                        is_active=True,
                    ),
                )
                await db.commit()

    for i, asset in enumerate(assets):
        src = os.path.join(root, asset.storage_relative_path.replace("/", os.sep))
        if not os.path.isfile(src):
            logger.error(
                "prompt_media_source_missing_for_actions",
                path=src,
                asset_id=str(asset.id),
            )
            continue
        dst_name = f"{i}_{_safe_filename(asset.original_filename)}"
        repo_path = f"{upload_dir}/{dst_name}"
        try:
            with open(src, "rb") as f:
                content = await asyncio.to_thread(f.read)
                files[repo_path] = content
            copied.append((dst_name, asset.kind))
            if cloud_client:
                cloud_ref = await cloud_client.upload_storage_file(
                    project_id=str(project_id),
                    bucket_name=bucket_name,
                    filename=asset.original_filename,
                    content_type=asset.mime_type,
                    content=content,
                )
                cloud_refs.append(
                    {
                        "asset_id": str(asset.id),
                        "original_filename": asset.original_filename,
                        "mime_type": asset.mime_type,
                        "byte_size": asset.byte_size,
                        "repo_path": repo_path,
                        **cloud_ref,
                    }
                )
        except OSError as e:
            logger.error("prompt_media_read_failed_for_actions", error=str(e), src=src)
        except Exception as e:
            logger.warning(
                "prompt_media_cloud_upload_failed_for_actions",
                error=str(e),
                asset_id=str(asset.id),
                project_id=str(project_id),
                run_id=str(run_id),
            )
            cloud_refs.append(
                {
                    "asset_id": str(asset.id),
                    "original_filename": asset.original_filename,
                    "mime_type": asset.mime_type,
                    "byte_size": asset.byte_size,
                    "repo_path": repo_path,
                    "ok": False,
                    "error": str(e)[:500],
                }
            )

    if copied:
        appendix = build_multimodal_prompt_appendix(
            voice_transcript=voice,
            copied_files=copied,
            repo_subdir=upload_dir,
        )
    else:
        loose = [(a.original_filename, a.kind) for a in assets] if assets else None
        appendix = build_multimodal_prompt_appendix(
            voice_transcript=voice,
            unattached_descriptions=loose,
        )

    appendix = appendix.strip()
    successful_cloud_refs = [r for r in cloud_refs if r.get("ok")]
    if successful_cloud_refs:
        cloud_lines = [
            "## Cloud storage refs for attached files",
            "These same attachments were uploaded to P2HS Cloud/R2 before codegen.",
            "Use `file_id` with the platform import/session APIs when importing CSV/JSON data.",
            "Signed download URLs are temporary convenience links; prefer `file_id` for "
            "durable app flows.",
            "",
        ]
        for ref in successful_cloud_refs:
            cloud_lines.append(
                "- "
                f"`{ref.get('original_filename')}`: "
                f"repo_path=`{ref.get('repo_path')}`, "
                f"bucket=`{ref.get('bucket')}`, "
                f"file_id=`{ref.get('file_id')}`"
            )
        appendix = (appendix + "\n\n" + "\n".join(cloud_lines)).strip()

    if not appendix:
        return PromptMediaActionsBundle(
            prompt=base_user_prompt,
            files=files,
            upload_dir=upload_dir if files else None,
            cloud_refs=cloud_refs,
        )

    base = (base_user_prompt or "").strip()
    prompt = appendix if not base else base + "\n\n" + appendix
    return PromptMediaActionsBundle(
        prompt=prompt,
        files=files,
        upload_dir=upload_dir if files else None,
        cloud_refs=cloud_refs,
    )
