"""Implement infrastructure support for json document processing history."""

from __future__ import annotations

import json
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from app.domain.documents import DocumentArtifact, DocumentProcessingRun, DocumentProcessingStore, DocumentStatusEvent
# Keep JSON persistence behavior aligned with the lightweight document repository adapter.


class JsonDocumentProcessingStore(DocumentProcessingStore):
    """Persist processing history in a standalone JSON file."""

    def __init__(self, file_path: str) -> None:
        """Initialize the JSON processing history store."""
        self.file_path = Path(file_path)
        self.file_path.parent.mkdir(parents=True, exist_ok=True)
        if not self.file_path.exists():
            self._save(self._empty_payload())

    def _empty_payload(self) -> dict[str, dict[str, dict[str, Any]]]:
        """Return the canonical empty JSON structure for processing history."""
        return {"runs": {}, "status_events": {}, "artifacts": {}}

    def _load(self) -> dict[str, dict[str, dict[str, Any]]]:
        """Load the full JSON payload and normalize missing sections."""
        if not self.file_path.exists():
            return self._empty_payload()
        payload = json.loads(self.file_path.read_text(encoding="utf-8") or "{}")
        normalized = self._empty_payload()
        for key in normalized:
            section = payload.get(key, {})
            normalized[key] = section if isinstance(section, dict) else {}
        return normalized

    def _save(self, payload: dict[str, dict[str, dict[str, Any]]]) -> None:
        """Persist the full JSON payload with stable formatting."""
        self.file_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")

    def _serialize_datetime(self, value: datetime | None) -> str | None:
        """Serialize optional datetimes into ISO8601 strings."""
        return value.isoformat() if value is not None else None

    def _deserialize_datetime(self, value: str | None) -> datetime | None:
        """Deserialize optional ISO8601 strings into datetimes."""
        return datetime.fromisoformat(value) if value else None

    def _serialize_run(self, run: DocumentProcessingRun) -> dict[str, Any]:
        """Serialize one processing run to a JSON-compatible payload."""
        return {
            "run_id": run.run_id,
            "doc_id": run.doc_id,
            "trigger_type": run.trigger_type,
            "run_status": run.run_status,
            "parser_backend": run.parser_backend,
            "chunk_backend": run.chunk_backend,
            "embedding_model": run.embedding_model,
            "index_name": run.index_name,
            "started_at": self._serialize_datetime(run.started_at),
            "stored_at": self._serialize_datetime(run.stored_at),
            "parsed_at": self._serialize_datetime(run.parsed_at),
            "indexed_at": self._serialize_datetime(run.indexed_at),
            "finished_at": self._serialize_datetime(run.finished_at),
            "layout_count": run.layout_count,
            "structure_node_count": run.structure_node_count,
            "semantic_block_count": run.semantic_block_count,
            "vector_chunk_count": run.vector_chunk_count,
            "chunk_count": run.chunk_count,
            "failure_stage": run.failure_stage,
            "error_message": run.error_message,
            "metadata": run.metadata,
        }

    def _deserialize_run(self, payload: dict[str, Any]) -> DocumentProcessingRun:
        """Deserialize one JSON payload into a processing run dataclass."""
        return DocumentProcessingRun(
            run_id=payload["run_id"],
            doc_id=payload["doc_id"],
            trigger_type=payload["trigger_type"],
            run_status=payload["run_status"],
            parser_backend=payload.get("parser_backend", ""),
            chunk_backend=payload.get("chunk_backend", ""),
            embedding_model=payload.get("embedding_model", ""),
            index_name=payload.get("index_name", ""),
            started_at=self._deserialize_datetime(payload.get("started_at")) or datetime.now(UTC),
            stored_at=self._deserialize_datetime(payload.get("stored_at")),
            parsed_at=self._deserialize_datetime(payload.get("parsed_at")),
            indexed_at=self._deserialize_datetime(payload.get("indexed_at")),
            finished_at=self._deserialize_datetime(payload.get("finished_at")),
            layout_count=int(payload.get("layout_count", 0) or 0),
            structure_node_count=int(payload.get("structure_node_count", 0) or 0),
            semantic_block_count=int(payload.get("semantic_block_count", 0) or 0),
            vector_chunk_count=int(payload.get("vector_chunk_count", 0) or 0),
            chunk_count=int(payload.get("chunk_count", 0) or 0),
            failure_stage=payload.get("failure_stage", ""),
            error_message=payload.get("error_message", ""),
            metadata=payload.get("metadata", {}),
        )

    def _serialize_event(self, event: DocumentStatusEvent) -> dict[str, Any]:
        """Serialize one status event to a JSON-compatible payload."""
        return {
            "event_id": event.event_id,
            "doc_id": event.doc_id,
            "run_id": event.run_id,
            "from_status": event.from_status,
            "to_status": event.to_status,
            "stage": event.stage,
            "message": event.message,
            "metadata": event.metadata,
            "occurred_at": self._serialize_datetime(event.occurred_at),
        }

    def _deserialize_event(self, payload: dict[str, Any]) -> DocumentStatusEvent:
        """Deserialize one JSON payload into a status event dataclass."""
        return DocumentStatusEvent(
            event_id=payload["event_id"],
            doc_id=payload["doc_id"],
            run_id=payload["run_id"],
            from_status=payload.get("from_status", ""),
            to_status=payload["to_status"],
            stage=payload.get("stage", ""),
            message=payload.get("message", ""),
            metadata=payload.get("metadata", {}),
            occurred_at=self._deserialize_datetime(payload.get("occurred_at")) or datetime.now(UTC),
        )

    def _serialize_artifact(self, artifact: DocumentArtifact) -> dict[str, Any]:
        """Serialize one artifact reference to a JSON-compatible payload."""
        return {
            "artifact_id": artifact.artifact_id,
            "doc_id": artifact.doc_id,
            "run_id": artifact.run_id,
            "artifact_type": artifact.artifact_type,
            "object_name": artifact.object_name,
            "content_type": artifact.content_type,
            "byte_size": artifact.byte_size,
            "checksum": artifact.checksum,
            "metadata": artifact.metadata,
            "created_at": self._serialize_datetime(artifact.created_at),
        }

    def _deserialize_artifact(self, payload: dict[str, Any]) -> DocumentArtifact:
        """Deserialize one JSON payload into an artifact dataclass."""
        return DocumentArtifact(
            artifact_id=payload["artifact_id"],
            doc_id=payload["doc_id"],
            run_id=payload["run_id"],
            artifact_type=payload["artifact_type"],
            object_name=payload["object_name"],
            content_type=payload.get("content_type", ""),
            byte_size=int(payload.get("byte_size", 0) or 0),
            checksum=payload.get("checksum", ""),
            metadata=payload.get("metadata", {}),
            created_at=self._deserialize_datetime(payload.get("created_at")) or datetime.now(UTC),
        )

    def _merge_metadata(self, original: dict[str, Any], update: dict | None) -> dict[str, Any]:
        """Merge metadata updates onto an existing payload."""
        merged = dict(original)
        if update:
            merged.update(update)
        return merged

    def create_run(self, run: DocumentProcessingRun) -> DocumentProcessingRun:
        """Create a new processing run record."""
        payload = self._load()
        payload["runs"][run.run_id] = self._serialize_run(run)
        self._save(payload)
        return run

    def mark_run_stored(
        self,
        run_id: str,
        *,
        stored_at: datetime | None = None,
        metadata: dict | None = None,
    ) -> DocumentProcessingRun | None:
        """Mark a run as having persisted the source file."""
        payload = self._load()
        run_payload = payload["runs"].get(run_id)
        if not run_payload:
            return None
        run = self._deserialize_run(run_payload)
        run.stored_at = stored_at or datetime.now(UTC)
        run.metadata = self._merge_metadata(run.metadata, metadata)
        payload["runs"][run_id] = self._serialize_run(run)
        self._save(payload)
        return run

    def mark_run_parsed(
        self,
        run_id: str,
        *,
        parser_backend: str,
        layout_count: int,
        structure_node_count: int,
        semantic_block_count: int,
        vector_chunk_count: int,
        parsed_at: datetime | None = None,
        metadata: dict | None = None,
    ) -> DocumentProcessingRun | None:
        """Record parse completion details for a run."""
        payload = self._load()
        run_payload = payload["runs"].get(run_id)
        if not run_payload:
            return None
        run = self._deserialize_run(run_payload)
        run.parser_backend = parser_backend
        run.layout_count = layout_count
        run.structure_node_count = structure_node_count
        run.semantic_block_count = semantic_block_count
        run.vector_chunk_count = vector_chunk_count
        run.parsed_at = parsed_at or datetime.now(UTC)
        run.metadata = self._merge_metadata(run.metadata, metadata)
        payload["runs"][run_id] = self._serialize_run(run)
        self._save(payload)
        return run

    def mark_run_indexed(
        self,
        run_id: str,
        *,
        chunk_count: int,
        index_name: str,
        indexed_at: datetime | None = None,
        finished_at: datetime | None = None,
        metadata: dict | None = None,
    ) -> DocumentProcessingRun | None:
        """Mark a run as successfully indexed."""
        payload = self._load()
        run_payload = payload["runs"].get(run_id)
        if not run_payload:
            return None
        run = self._deserialize_run(run_payload)
        now = datetime.now(UTC)
        run.run_status = "succeeded"
        run.chunk_count = chunk_count
        run.index_name = index_name
        run.indexed_at = indexed_at or now
        run.finished_at = finished_at or now
        run.metadata = self._merge_metadata(run.metadata, metadata)
        payload["runs"][run_id] = self._serialize_run(run)
        self._save(payload)
        return run

    def mark_run_failed(
        self,
        run_id: str,
        *,
        failure_stage: str,
        error_message: str,
        finished_at: datetime | None = None,
        metadata: dict | None = None,
    ) -> DocumentProcessingRun | None:
        """Mark a run as failed."""
        payload = self._load()
        run_payload = payload["runs"].get(run_id)
        if not run_payload:
            return None
        run = self._deserialize_run(run_payload)
        run.run_status = "failed"
        run.failure_stage = failure_stage
        run.error_message = error_message
        run.finished_at = finished_at or datetime.now(UTC)
        run.metadata = self._merge_metadata(run.metadata, metadata)
        payload["runs"][run_id] = self._serialize_run(run)
        self._save(payload)
        return run

    def append_status_event(self, event: DocumentStatusEvent) -> DocumentStatusEvent:
        """Append a document status event."""
        payload = self._load()
        payload["status_events"][event.event_id] = self._serialize_event(event)
        self._save(payload)
        return event

    def replace_artifacts_for_run(self, run_id: str, artifacts: list[DocumentArtifact]) -> list[DocumentArtifact]:
        """Replace all artifacts for a run with the provided list."""
        payload = self._load()
        payload["artifacts"] = {
            artifact_id: artifact_payload
            for artifact_id, artifact_payload in payload["artifacts"].items()
            if artifact_payload.get("run_id") != run_id
        }
        for artifact in artifacts:
            payload["artifacts"][artifact.artifact_id] = self._serialize_artifact(artifact)
        self._save(payload)
        return artifacts

    def delete_by_document(self, doc_id: str) -> None:
        """Delete all processing data for a document."""
        payload = self._load()
        payload["runs"] = {
            run_id: run_payload
            for run_id, run_payload in payload["runs"].items()
            if run_payload.get("doc_id") != doc_id
        }
        payload["status_events"] = {
            event_id: event_payload
            for event_id, event_payload in payload["status_events"].items()
            if event_payload.get("doc_id") != doc_id
        }
        payload["artifacts"] = {
            artifact_id: artifact_payload
            for artifact_id, artifact_payload in payload["artifacts"].items()
            if artifact_payload.get("doc_id") != doc_id
        }
        self._save(payload)

    def list_runs_by_document(self, doc_id: str) -> list[DocumentProcessingRun]:
        """List all processing runs for a document."""
        payload = self._load()
        runs = [
            self._deserialize_run(run_payload)
            for run_payload in payload["runs"].values()
            if run_payload.get("doc_id") == doc_id
        ]
        runs.sort(key=lambda run: run.started_at)
        return runs

    def get_run(self, run_id: str) -> DocumentProcessingRun | None:
        """Return one processing run by identifier."""
        payload = self._load()
        run_payload = payload["runs"].get(run_id)
        return self._deserialize_run(run_payload) if run_payload else None

    def list_status_events_by_document(self, doc_id: str) -> list[DocumentStatusEvent]:
        """List status events for a document."""
        payload = self._load()
        events = [
            self._deserialize_event(event_payload)
            for event_payload in payload["status_events"].values()
            if event_payload.get("doc_id") == doc_id
        ]
        events.sort(key=lambda event: event.occurred_at)
        return events

    def list_status_events_by_run(self, run_id: str) -> list[DocumentStatusEvent]:
        """List status events for a run."""
        payload = self._load()
        events = [
            self._deserialize_event(event_payload)
            for event_payload in payload["status_events"].values()
            if event_payload.get("run_id") == run_id
        ]
        events.sort(key=lambda event: event.occurred_at)
        return events

    def list_artifacts_by_document(self, doc_id: str) -> list[DocumentArtifact]:
        """List artifact references for a document."""
        payload = self._load()
        artifacts = [
            self._deserialize_artifact(artifact_payload)
            for artifact_payload in payload["artifacts"].values()
            if artifact_payload.get("doc_id") == doc_id
        ]
        artifacts.sort(key=lambda artifact: artifact.created_at)
        return artifacts

    def list_artifacts_by_run(self, run_id: str) -> list[DocumentArtifact]:
        """List artifact references for a run."""
        payload = self._load()
        artifacts = [
            self._deserialize_artifact(artifact_payload)
            for artifact_payload in payload["artifacts"].values()
            if artifact_payload.get("run_id") == run_id
        ]
        artifacts.sort(key=lambda artifact: artifact.created_at)
        return artifacts
