"""ARR-owned execution of the frozen Opera daily processor."""

from __future__ import annotations

import subprocess
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Dict, Mapping, Optional

from arr_ingestion.contracts import IngestionError
from arr_ingestion.validation import ProcessorPolicy, strict_json_file


@dataclass(frozen=True)
class LocalProcessingOutput:
    status: str
    business_date: Optional[date]
    artifacts: Mapping[str, Path]
    exit_code: int


@dataclass(frozen=True)
class LocalDailyProcessor:
    """Run one isolated deterministic process and expose only its artifact paths."""

    policy: ProcessorPolicy
    timeout_seconds: int = 240

    def __post_init__(self) -> None:
        if (
            not isinstance(self.timeout_seconds, int)
            or isinstance(self.timeout_seconds, bool)
            or not 10 <= self.timeout_seconds <= 900
        ):
            raise ValueError("local processor timeout is invalid")
        if not (self.policy.skill_root / "scripts" / "process_daily.py").is_file():
            raise ValueError("ARR daily processor is unavailable")

    def run(
        self,
        source_xml: Path,
        output_dir: Path,
        *,
        manual_overrides: Optional[Path] = None,
        review_job_id: Optional[str] = None,
        review_case_id: Optional[str] = None,
        manual_override_sha256: Optional[str] = None,
    ) -> LocalProcessingOutput:
        if not source_xml.is_file():
            raise IngestionError("SOURCE_NOT_FOUND", "registered source XML is unavailable")
        manual_arguments = (
            manual_overrides,
            review_job_id,
            review_case_id,
            manual_override_sha256,
        )
        if any(value is not None for value in manual_arguments):
            if (
                manual_overrides is None
                or not manual_overrides.is_file()
                or not review_job_id
                or not review_case_id
                or not manual_override_sha256
            ):
                raise IngestionError(
                    "MANUAL_OVERRIDE_INVALID",
                    "frozen manual-price overrides are unavailable",
                )
        output_dir.mkdir(parents=True, exist_ok=False, mode=0o700)
        result_path = output_dir / "result.json"
        structured_path = output_dir / "structured-result.json"
        command = [
            self.policy.python_binary,
            str((self.policy.skill_root / "scripts" / "process_daily.py").resolve()),
            "--xml",
            str(source_xml.resolve()),
            "--output-dir",
            str(output_dir.resolve()),
            "--result-json",
            str(result_path.resolve()),
            "--structured-result-json",
            str(structured_path.resolve()),
        ]
        if manual_overrides is not None:
            command.extend(
                (
                    "--manual-override-json",
                    str(manual_overrides.resolve()),
                    "--review-job-id",
                    str(review_job_id),
                    "--review-case-id",
                    str(review_case_id),
                    "--manual-override-sha256",
                    str(manual_override_sha256),
                )
            )
        try:
            completed = subprocess.run(
                command,
                stdin=subprocess.DEVNULL,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                check=False,
                timeout=self.timeout_seconds,
            )
        except subprocess.TimeoutExpired:
            raise IngestionError(
                "PROCESSOR_TIMEOUT",
                "deterministic XML processing exceeded its time limit",
                retryable=True,
            ) from None
        except OSError:
            raise IngestionError(
                "PROCESSOR_UNAVAILABLE",
                "deterministic XML processing could not start",
                retryable=True,
            ) from None

        result = strict_json_file(result_path, "processor result")
        structured = strict_json_file(structured_path, "structured processor result")
        status = result.get("status")
        if (
            status not in {"success", "failed", "review_required"}
            or structured.get("status") != status
            or (completed.returncode == 0) != (status in {"success", "review_required"})
        ):
            raise IngestionError(
                "PROCESSOR_RESULT_INVALID",
                "processor exit status and result artifacts do not agree",
            )

        raw_date = result.get("business_date")
        business_date: Optional[date]
        if raw_date is None:
            business_date = None
        elif isinstance(raw_date, str):
            try:
                business_date = date.fromisoformat(raw_date)
            except ValueError:
                raise IngestionError(
                    "PROCESSOR_RESULT_INVALID", "processor business date is invalid"
                ) from None
        else:
            raise IngestionError(
                "PROCESSOR_RESULT_INVALID", "processor business date is invalid"
            )

        outputs = result.get("outputs")
        if not isinstance(outputs, dict):
            raise IngestionError(
                "PROCESSOR_RESULT_INVALID", "processor output manifest is invalid"
            )
        artifacts: Dict[str, Path] = {
            "result_json": result_path,
            "structured_result_json": structured_path,
        }
        if status == "success":
            artifacts["daily_report"] = self._output_path(
                output_dir, outputs.get("daily_report"), ".xlsx"
            )
            if outputs.get("exception_report") is not None or business_date is None:
                raise IngestionError(
                    "PROCESSOR_RESULT_INVALID", "successful processor output is incomplete"
                )
        elif status == "review_required":
            if (
                business_date is None
                or outputs.get("daily_report") is not None
                or outputs.get("exception_report") is not None
            ):
                raise IngestionError(
                    "PROCESSOR_RESULT_INVALID", "review processor output is inconsistent"
                )
        else:
            artifacts["exception_report"] = self._output_path(
                output_dir, outputs.get("exception_report"), ".xlsx"
            )
            if outputs.get("daily_report") is not None:
                raise IngestionError(
                    "PROCESSOR_RESULT_INVALID", "failed processor output is inconsistent"
                )
        return LocalProcessingOutput(
            status=str(status),
            business_date=business_date,
            artifacts=artifacts,
            exit_code=completed.returncode,
        )

    @staticmethod
    def _output_path(output_dir: Path, value: object, suffix: str) -> Path:
        if (
            not isinstance(value, str)
            or not value
            or Path(value).name != value
            or not value.lower().endswith(suffix)
        ):
            raise IngestionError(
                "PROCESSOR_RESULT_INVALID", "processor output filename is invalid"
            )
        candidate = (output_dir / value).resolve()
        if candidate.parent != output_dir.resolve() or not candidate.is_file():
            raise IngestionError(
                "PROCESSOR_RESULT_INVALID", "processor output artifact is unavailable"
            )
        return candidate
