from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

from youtube_cli_agent.errors import AgentError, ValidationError
from youtube_cli_agent.markdown_logger import MarkdownLogger
from youtube_cli_agent.models import ExecutionSummary, TranscriptResult, VideoCandidate, VideoMetadata
from youtube_cli_agent.tools import (
    AudioDownloadTool,
    CaptionRetrievalTool,
    DuplicateCheckerTool,
    MarkdownPersistenceTool,
    MetadataValidationTool,
    TranscriptionTool,
    YouTubeSearchTool,
)


@dataclass(frozen=True, slots=True)
class ExecutionRequest:
    query: str
    limit: int = 10
    select_count: int = 1
    selected_video_ids: tuple[str, ...] = ()
    output_dir: Path = Path("transcripts")
    audio_cache_dir: Path = Path(".cache/audio")
    fallback_language: str | None = None


@dataclass(slots=True)
class Orchestrator:
    search_tool: YouTubeSearchTool
    metadata_tool: MetadataValidationTool
    caption_tool: CaptionRetrievalTool
    audio_tool: AudioDownloadTool
    transcription_tool: TranscriptionTool
    persistence_tool: MarkdownPersistenceTool
    dedupe_tool: DuplicateCheckerTool

    def run(self, request: ExecutionRequest) -> tuple[bool, str]:
        logger = MarkdownLogger()
        summary = ExecutionSummary(query=request.query)
        logger.header(request.query)

        try:
            logger.stage(
                name="Stage 1: Search",
                action=f"Search YouTube for top {request.limit} videos.",
                tool="YouTubeSearchTool",
            )
            candidates = self.search_tool.run(query=request.query, limit=request.limit)
            logger.info(f"Retrieved {len(candidates)} candidate videos.")
            self._validate_search_results(candidates, request.limit)
            logger.validation(True, "Search returned deterministic ranked candidates.")

            logger.stage(
                name="Stage 2: Selection",
                action="Deterministically select videos for processing.",
                tool="SelectionRule",
            )
            selected = self._select_candidates(candidates, request)
            summary.attempted = len(selected)
            logger.info(
                f"Selected {len(selected)} video(s): {', '.join(candidate.video_id for candidate in selected)}"
            )
            logger.validation(True, "Selection is deterministic and reproducible.")

            for index, candidate in enumerate(selected, start=1):
                self._process_video(
                    index=index,
                    total=len(selected),
                    candidate=candidate,
                    request=request,
                    logger=logger,
                    summary=summary,
                )

            logger.final_success(
                [
                    f"Attempted videos: {summary.attempted}",
                    f"Processed videos: {summary.processed}",
                    f"Skipped videos: {summary.skipped}",
                    f"Failed videos: {summary.failed}",
                    f"Output files: {', '.join(str(p) for p in summary.output_files) if summary.output_files else 'None'}",
                ]
            )
            return True, logger.render()

        except AgentError as exc:
            summary.failed += 1
            logger.final_error(stage=exc.stage, code=exc.code, message=exc.message)
            return False, logger.render()

    def _process_video(
        self,
        *,
        index: int,
        total: int,
        candidate: VideoCandidate,
        request: ExecutionRequest,
        logger: MarkdownLogger,
        summary: ExecutionSummary,
    ) -> None:
        stage_prefix = f"Video {index}/{total} ({candidate.video_id})"

        logger.stage(
            name=f"{stage_prefix} — Stage 3: Metadata Validation",
            action="Fetch and validate canonical metadata.",
            tool="MetadataValidationTool",
        )
        metadata = self.metadata_tool.run(candidate)
        self._validate_metadata(metadata)
        logger.info(f"Metadata validated for '{metadata.title}' by {metadata.channel}.")
        logger.validation(True, "Required metadata fields are complete and valid.")

        target_path = self.persistence_tool.target_path(request.output_dir, metadata)
        logger.stage(
            name=f"{stage_prefix} — Stage 4: Duplicate Check",
            action="Check existing transcript file and integrity.",
            tool="DuplicateCheckerTool",
        )
        check_result = self.dedupe_tool.run(target_path)
        logger.info(check_result.reason)

        if check_result.exists and check_result.is_valid and check_result.has_transcript:
            summary.skipped += 1
            summary.output_files.append(target_path)
            logger.validation(True, "Skipped — Transcript already exists and is valid.")
            return

        overwrite = check_result.exists and check_result.is_corrupt
        if overwrite:
            logger.validation(True, "Existing file is corrupt and will be overwritten.")
        else:
            logger.validation(True, "No valid transcript file exists; processing continues.")

        logger.stage(
            name=f"{stage_prefix} — Stage 5: Caption Retrieval",
            action="Retrieve official captions using deterministic priority and probe checks.",
            tool="CaptionRetrievalTool",
        )
        caption_result = self.caption_tool.run(metadata)
        logger.info(
            "Caption probe summary: "
            f"captions_found={caption_result.probe_summary.captions_found}, "
            f"region_restricted={caption_result.probe_summary.region_restricted}, "
            f"notes={caption_result.probe_summary.notes}"
        )

        transcript: TranscriptResult
        if caption_result.transcript is not None:
            transcript = caption_result.transcript
            logger.validation(True, "Official captions retrieved and selected.")
        else:
            if not caption_result.verified_unavailable:
                raise ValidationError(
                    code="CAPTION_AVAILABILITY_UNCERTAIN",
                    stage="Caption Retrieval",
                    message=(
                        "Captions were not retrieved, but availability is uncertain "
                        "(possible restriction or fetch anomaly)."
                    ),
                )
            logger.validation(True, "Captions verified unavailable across probes; fallback permitted.")

            logger.stage(
                name=f"{stage_prefix} — Stage 6: Audio Download",
                action="Download audio only because captions are unavailable.",
                tool="AudioDownloadTool",
            )
            audio_path, from_cache = self.audio_tool.run(metadata)
            logger.info(f"Audio path: `{audio_path}`; reused_cache={from_cache}")
            logger.validation(True, "Audio file exists and passed non-empty integrity check.")

            logger.stage(
                name=f"{stage_prefix} — Stage 7: Transcription",
                action="Run automatic transcription fallback.",
                tool="TranscriptionTool",
            )
            transcript = self.transcription_tool.run(audio_path, language=request.fallback_language)
            logger.info("Automatic transcription completed.")
            logger.validation(True, "Fallback transcript passed minimum length threshold.")

        logger.stage(
            name=f"{stage_prefix} — Stage 8: Markdown Persistence",
            action="Persist deterministic markdown output.",
            tool="MarkdownPersistenceTool",
        )
        persist_result = self.persistence_tool.run(
            output_dir=request.output_dir,
            metadata=metadata,
            transcript=transcript,
            overwrite=overwrite,
        )
        logger.info(persist_result.reason)

        post_check = self.dedupe_tool.run(persist_result.path)
        if not post_check.is_valid:
            raise ValidationError(
                code="WRITE_VALIDATION_FAILED",
                stage="Markdown Persistence",
                message=f"File written but integrity validation failed: {post_check.reason}",
            )

        logger.validation(True, "Markdown file written and integrity verified.")
        summary.processed += 1
        summary.output_files.append(persist_result.path)

    @staticmethod
    def _validate_search_results(results: list[VideoCandidate], limit: int) -> None:
        if not results:
            raise ValidationError(
                code="NO_RESULTS",
                stage="Search",
                message="Search stage returned zero candidates.",
            )
        if len(results) > limit:
            raise ValidationError(
                code="RESULT_LIMIT_EXCEEDED",
                stage="Search",
                message="Search returned more results than requested limit.",
            )

        expected_rank = 1
        for result in results:
            if result.rank != expected_rank:
                raise ValidationError(
                    code="NON_DETERMINISTIC_RANK",
                    stage="Search",
                    message="Search ranking order is not deterministic.",
                )
            expected_rank += 1

    @staticmethod
    def _select_candidates(
        candidates: list[VideoCandidate],
        request: ExecutionRequest,
    ) -> list[VideoCandidate]:
        if request.selected_video_ids:
            index = {candidate.video_id: candidate for candidate in candidates}
            selected = [index[video_id] for video_id in request.selected_video_ids if video_id in index]
            if not selected:
                raise ValidationError(
                    code="SELECTION_EMPTY",
                    stage="Selection",
                    message="No selected video IDs matched search results.",
                )
            return selected

        selected = candidates[: request.select_count]
        if not selected:
            raise ValidationError(
                code="SELECTION_EMPTY",
                stage="Selection",
                message="Deterministic selection produced zero videos.",
            )
        return selected

    @staticmethod
    def _validate_metadata(metadata: VideoMetadata) -> None:
        required = {
            "video_id": metadata.video_id,
            "title": metadata.title,
            "channel": metadata.channel,
            "url": metadata.url,
            "publish_date": metadata.publish_date,
            "description": metadata.description,
        }
        missing = [name for name, value in required.items() if not str(value).strip()]
        if missing:
            raise ValidationError(
                code="METADATA_INCOMPLETE",
                stage="Metadata Validation",
                message=f"Metadata is missing required fields: {', '.join(missing)}",
            )
        if metadata.duration_seconds <= 0:
            raise ValidationError(
                code="METADATA_INCOMPLETE",
                stage="Metadata Validation",
                message="Metadata duration_seconds must be greater than zero.",
            )
