"""에피소드 관리 서비스."""

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,
    EntityEpisodeLink,
    ImageAsset,
    PipelineProgress,
    SceneStill,
    WebbookPackage,
    WorldGuide,
)
from app.modules.pdf_parser import extract_text_from_pdf, detect_language


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


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


class EpisodeService:
    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

    # -- queries -----------------------------------------------------------

    def list_episodes(self) -> List[Episode]:
        return (
            self._db.query(Episode)
            .filter(Episode.project_id == self._project_id)
            .order_by(Episode.episode_number)
            .all()
        )

    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

    # -- mutations ---------------------------------------------------------

    def create_episode(
        self,
        episode_number: int,
        title: str,
        pdf_file_bytes: bytes,
        filename: str,
        ip: Optional[str] = None,
    ) -> Episode:
        # Check duplicate episode number within project
        existing = (
            self._db.query(Episode)
            .filter(Episode.project_id == self._project_id, Episode.episode_number == episode_number)
            .first()
        )
        if existing:
            raise AppError(
                code="episode.duplicate_number",
                message=t("episode.duplicate_number"),
                status_code=400,
            )

        # Save PDF file — 파일명 path traversal 방지
        safe_filename = Path(filename).name  # ../../ 등 경로 컴포넌트 제거
        if not safe_filename:
            safe_filename = "uploaded.pdf"
        screenplay_dir = Path(settings.projects_dir) / self._project_id / "assets" / "screenplays"
        screenplay_dir.mkdir(parents=True, exist_ok=True)
        pdf_path = screenplay_dir / safe_filename

        # Avoid filename collision
        if pdf_path.exists():
            stem = pdf_path.stem
            suffix = pdf_path.suffix
            pdf_path = screenplay_dir / f"{stem}_{_new_id()[:8]}{suffix}"

        try:
            pdf_path.write_bytes(pdf_file_bytes)
        except Exception:
            raise AppError(
                code="episode.upload_failed",
                message=t("episode.upload_failed"),
                status_code=500,
            )

        # Extract text from PDF
        try:
            fulltext, page_count = extract_text_from_pdf(pdf_path)
        except Exception:
            raise AppError(
                code="episode.invalid_pdf",
                message=t("episode.invalid_pdf"),
                status_code=400,
            )

        language = detect_language(fulltext)

        now = _now()
        episode_id = _new_id()
        episode = Episode(
            id=episode_id,
            project_id=self._project_id,
            episode_number=episode_number,
            title=title,
            source_filename=filename,
            source_path=str(pdf_path),
            fulltext=fulltext,
            language=language,
            page_count=page_count,
            status="uploaded",
            created_at=now,
            updated_at=now,
        )
        self._db.add(episode)
        self._db.commit()

        self._logger.log(
            actor_id=self._actor_id,
            action="episode.create",
            resource_type="episode",
            resource_id=episode_id,
            project_id=self._project_id,
            detail={"episode_number": episode_number, "title": title, "filename": filename},
            ip_address=ip,
        )

        return episode

    def update_episode(
        self,
        episode_id: str,
        updates: Dict[str, Any],
        ip: Optional[str] = None,
    ) -> Episode:
        episode = self.get_episode(episode_id)

        before: Dict[str, Any] = {}
        after: Dict[str, Any] = {}

        if "episode_number" in updates and updates["episode_number"] is not None:
            new_num = updates["episode_number"]
            if new_num != episode.episode_number:
                dup = (
                    self._db.query(Episode)
                    .filter(
                        Episode.project_id == self._project_id,
                        Episode.episode_number == new_num,
                        Episode.id != episode_id,
                    )
                    .first()
                )
                if dup:
                    raise AppError(
                        code="episode.duplicate_number",
                        message=t("episode.duplicate_number"),
                        status_code=400,
                    )
                before["episode_number"] = episode.episode_number
                episode.episode_number = new_num
                after["episode_number"] = new_num

        if "title" in updates and updates["title"] is not None:
            before["title"] = episode.title
            episode.title = updates["title"]
            after["title"] = updates["title"]

        episode.updated_at = _now()
        self._db.commit()

        self._logger.log(
            actor_id=self._actor_id,
            action="episode.update",
            resource_type="episode",
            resource_id=episode_id,
            project_id=self._project_id,
            detail={"before": before, "after": after},
            ip_address=ip,
        )

        return episode

    def delete_episode(
        self,
        episode_id: str,
        ip: Optional[str] = None,
    ) -> None:
        episode = self.get_episode(episode_id)

        # Remove the source file if it exists (with path traversal guard)
        source = Path(episode.source_path)
        resolved_source = source.resolve()
        project_dir = Path(settings.projects_dir).resolve()
        if str(resolved_source).startswith(str(project_dir) + "/") and source.exists():
            source.unlink()

        # Cascade delete related data before removing the Episode row
        for model in (
            SceneStill,
            EntityEpisodeLink,
            WorldGuide,
            WebbookPackage,
            ImageAsset,
            PipelineProgress,
        ):
            self._db.query(model).filter(model.episode_id == episode_id).delete()

        self._db.delete(episode)
        self._db.commit()

        self._logger.log(
            actor_id=self._actor_id,
            action="episode.delete",
            resource_type="episode",
            resource_id=episode_id,
            project_id=self._project_id,
            detail={"episode_number": episode.episode_number, "title": episode.title},
            ip_address=ip,
        )
