"""내보내기 서비스 — 웹북 생성 및 PDF 렌더링 파이프라인."""

import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from sqlalchemy.orm import Session as OrmSession

from app.core.config import settings
from app.core.errors import AppError
from app.i18n.loader import t
from app.logging.activity_logger import ActivityLogger
from app.models.project import (
    Episode,
    EntityCanon,
    EntityEpisodeLink,
    ImageAsset,
    SceneStill,
    WebbookPackage,
    WorldGuide,
)
from app.modules.llm.openai_client import OpenAIClient
from app.modules.pdf_renderer import PDFRenderer
from app.modules.pdf_validator import PDFValidator
from app.modules.progress_tracker import ProgressTracker
from app.modules.provenance import ProvenanceRecorder
from app.modules.webbook_generator import WebbookGenerator


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _new_id() -> str:
    return str(uuid.uuid4())


class ExportService:
    """웹북 생성 및 PDF 렌더링 서비스."""

    def __init__(
        self,
        db: OrmSession,
        project_id: str,
        actor_id: str,
    ) -> None:
        self._db = db
        self._logger = ActivityLogger(db)
        self._project_id = project_id
        self._actor_id = actor_id

    def _get_episode(self, episode_id: str) -> Episode:
        ep = (
            self._db.query(Episode)
            .filter(Episode.id == episode_id, Episode.project_id == self._project_id)
            .first()
        )
        if not ep:
            raise AppError(
                code="episode.not_found",
                message=t("episode.not_found"),
                status_code=404,
            )
        return ep

    def _get_project_dir(self) -> Path:
        return Path(settings.projects_dir) / self._project_id

    def _get_exports_dir(self) -> Path:
        d = self._get_project_dir() / "assets" / "exports"
        d.mkdir(parents=True, exist_ok=True)
        return d

    def generate_webbook(
        self,
        episode_id: str,
        web_episode_count: int = 4,
        sections_per_episode: int = 10,
        ip: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Generate webbook package for an episode."""
        episode = self._get_episode(episode_id)

        if not episode.fulltext:
            raise AppError(
                code="analysis.no_text",
                message=t("analysis.no_text"),
                status_code=400,
            )

        if not settings.openai_api_key:
            raise AppError(
                code="analysis.openai_key_missing",
                message=t("analysis.openai_key_missing"),
                status_code=400,
            )

        language = episode.language or "ko"

        # Get entities
        links = (
            self._db.query(EntityEpisodeLink)
            .filter(
                EntityEpisodeLink.project_id == self._project_id,
                EntityEpisodeLink.episode_id == episode_id,
            )
            .all()
        )
        canon_ids = [link.canon_id for link in links]
        entities_orm = (
            self._db.query(EntityCanon)
            .filter(EntityCanon.id.in_(canon_ids))
            .all()
            if canon_ids
            else []
        )
        entities = [
            {
                "id": e.id,
                "name": e.name,
                "entity_type": e.entity_type,
                "description": e.description or "",
                "stable_traits": e.stable_traits or "{}",
            }
            for e in entities_orm
        ]

        # Get scene stills
        stills_orm = (
            self._db.query(SceneStill)
            .filter(SceneStill.project_id == self._project_id, SceneStill.episode_id == episode_id)
            .order_by(SceneStill.still_index)
            .all()
        )
        stills = [
            {
                "id": s.id,
                "still_index": s.still_index,
                "screenplay_scene_heading": s.screenplay_scene_heading or "",
                "beat_title": s.beat_title or "",
                "still_frame_prompt": s.still_frame_prompt or "",
            }
            for s in stills_orm
        ]

        # Get world guide
        wg_record = (
            self._db.query(WorldGuide)
            .filter(WorldGuide.project_id == self._project_id, WorldGuide.episode_id == episode_id)
            .order_by(WorldGuide.created_at.desc())
            .first()
        )
        world_guide = (
            json.loads(wg_record.guide_json) if wg_record else {}
        )

        # Generate webbook package
        provenance = ProvenanceRecorder(self._db, self._project_id)
        progress = ProgressTracker(self._db, episode_id, "webbook", self._project_id)
        progress.update("웹북 텍스트 생성 중", 0, 1)

        openai_client = OpenAIClient()
        generator = WebbookGenerator(llm_client=openai_client)
        try:
            with provenance.start_operation(
                "webbook_generation", "webbook_generator", episode_id=episode_id,
            ) as op:
                op.set_input({
                    "fulltext_chars": len(episode.fulltext),
                    "entities": len(entities),
                    "stills": len(stills),
                    "web_episode_count": web_episode_count,
                    "sections_per_episode": sections_per_episode,
                })
                package = generator.generate(
                    fulltext=episode.fulltext,
                    entities=entities,
                    stills=stills,
                    world_guide=world_guide,
                    language=language,
                    web_episode_count=web_episode_count,
                    sections_per_episode=sections_per_episode,
                )
                ep_count = len(package.get("episodes", []))
                op.set_output({"episodes_generated": ep_count})

            # Save to DB
            wb_record = WebbookPackage(
                id=_new_id(),
                project_id=self._project_id,
                episode_id=episode_id,
                package_json=json.dumps(package, ensure_ascii=False),
                prompt_version="v5",
                created_at=_now(),
            )
            self._db.add(wb_record)
            self._db.commit()

            progress.complete()
        except Exception as exc:
            progress.fail(str(exc))
            raise

        self._logger.log(
            actor_id=self._actor_id,
            action="export.generate_webbook",
            resource_type="episode",
            resource_id=episode_id,
            project_id=self._project_id,
            detail={
                "web_episode_count": web_episode_count,
                "sections_per_episode": sections_per_episode,
            },
            ip_address=ip,
        )

        return package

    def render_pdfs(
        self,
        episode_id: str,
        ip: Optional[str] = None,
    ) -> List[str]:
        """Render PDFs from existing webbook package + images."""
        self._get_episode(episode_id)

        # Get latest webbook package
        wb_record = (
            self._db.query(WebbookPackage)
            .filter(
                WebbookPackage.project_id == self._project_id,
                WebbookPackage.episode_id == episode_id,
            )
            .order_by(WebbookPackage.created_at.desc())
            .first()
        )
        if not wb_record:
            raise AppError(
                code="export.no_webbook",
                message=t("export.no_webbook"),
                status_code=400,
            )

        package = json.loads(wb_record.package_json)

        # Build image map: still_id -> file_path
        # For each still, prefer is_primary=1, then most recent original variant
        images_orm = (
            self._db.query(ImageAsset)
            .filter(
                ImageAsset.project_id == self._project_id,
                ImageAsset.episode_id == episode_id,
                ImageAsset.asset_type == "scene",
            )
            .all()
        )

        # Group images by still_id
        still_images: Dict[str, list] = {}
        for img in images_orm:
            if img.still_id and img.status in ("generated", "approved"):
                still_images.setdefault(img.still_id, []).append(img)

        image_map: Dict[str, str] = {}
        for still_id, imgs in still_images.items():
            # 1. Find primary image
            primary = next((i for i in imgs if i.is_primary), None)
            if primary:
                image_map[still_id] = primary.file_path
                continue
            # 2. Fallback: most recent original variant
            originals = [i for i in imgs if (i.variant_type or "original") == "original"]
            if originals:
                originals.sort(key=lambda i: i.created_at or "", reverse=True)
                image_map[still_id] = originals[0].file_path
                continue
            # 3. Last fallback: most recent image of any variant
            imgs.sort(key=lambda i: i.created_at or "", reverse=True)
            image_map[still_id] = imgs[0].file_path

        # Render PDFs
        exports_dir = self._get_exports_dir()
        renderer = PDFRenderer()
        provenance = ProvenanceRecorder(self._db, self._project_id)
        progress = ProgressTracker(self._db, episode_id, "pdf_render", self._project_id)
        progress.update("PDF 렌더링 중", 0, 1)

        try:
            with provenance.start_operation(
                "pdf_rendering", "pdf_renderer", episode_id=episode_id,
            ) as op:
                op.set_input({"episodes_in_package": len(package.get("episodes", [])), "images_available": len(image_map)})
                paths = renderer.render_all(
                    package=package,
                    images=image_map,
                    output_dir=exports_dir,
                )
                op.set_output({"pdf_count": len(paths)})

            progress.complete()
        except Exception as exc:
            progress.fail(str(exc))
            raise

        result = [str(p) for p in paths]

        self._logger.log(
            actor_id=self._actor_id,
            action="export.render_pdf",
            resource_type="episode",
            resource_id=episode_id,
            project_id=self._project_id,
            detail={"pdf_count": len(result)},
            ip_address=ip,
        )

        return result

    def validate_pdf(
        self,
        filename: str,
        ip: Optional[str] = None,
    ) -> Dict[str, Any]:
        if not settings.openai_api_key:
            raise AppError(
                code="analysis.openai_key_missing",
                message=t("analysis.openai_key_missing"),
                status_code=400,
            )

        exports_dir = self._get_exports_dir()
        safe_filename = Path(filename).name  # path traversal 방지
        pdf_path = exports_dir / safe_filename

        # resolve() 후 exports_dir 하위인지 검증
        if not pdf_path.resolve().is_relative_to(exports_dir.resolve()):
            raise AppError(code="export.invalid_path", message="Invalid filename", status_code=400)
        if not pdf_path.exists() or not pdf_path.is_file():
            raise AppError(
                code="export.file_not_found",
                message=t("export.file_not_found"),
                status_code=404,
            )

        validator = PDFValidator()
        result = validator.validate(pdf_path)

        self._logger.log(
            actor_id=self._actor_id,
            action="export.validate_pdf",
            resource_type="export",
            resource_id=filename,
            project_id=self._project_id,
            detail={
                "overall_quality": result.get("overall_quality", 0),
                "issues_count": len(result.get("issues", [])),
            },
            ip_address=ip,
        )

        return result

    def list_exports(self) -> List[Dict[str, Any]]:
        """List all exported files."""
        exports_dir = self._get_exports_dir()
        result: List[Dict[str, Any]] = []

        if not exports_dir.exists():
            return result

        for f in sorted(exports_dir.iterdir()):
            if f.is_file() and f.suffix == ".pdf":
                stat = f.stat()
                result.append(
                    {
                        "filename": f.name,
                        "episode_id": None,
                        "file_path": str(f),
                        "size_bytes": stat.st_size,
                        "created_at": datetime.fromtimestamp(
                            stat.st_mtime, tz=timezone.utc
                        ).isoformat(),
                    }
                )

        return result
