"""W20A: BaseLocationDossierStep — deterministic per-fp dossier producer.

Consumes:
  - ``floor_plan_prompt`` (v6) — numbered_elements + base_layer_decision.
  - ``floor_plan_render`` — png_path per fp (best-effort).
  - ``background_master_plan`` — backgrounds[] DAG per fp.
  - ``floor_plan_overlay_payload`` — per-bg overlay payload.

Gates (opt-in, default OFF):
  - ``settings.background_mode`` ∉ {"on","floor_plan_anchored"} → not_applicable.
  - ``settings.base_location_dossier_enabled`` != True → not_applicable.
  - ``settings.floor_plan_prompt_version`` != "6" → not_applicable.
    v5 path does not carry base_layer_decision, so a dossier built off
    v5 markers would lose the partition contract.

Checkpoint data shape:
  ``data.dossiers`` = ``{fp_id: dossier_dict}`` (build_dossiers output).
  Failure path emits ``data.error`` (truncated str) and an empty
  ``data.dossiers``.

LLM / image / VLM API call 0, DB / ImageAsset write 0.
"""
from __future__ import annotations

import hashlib
import json
import logging
from pathlib import Path
from typing import Any, Dict, Optional

from app.core.step_runner import StepRunner
from app.modules.pipeline.base_location_dossier import (
    BaseLocationDossierError,
    build_dossiers,
)

logger = logging.getLogger(__name__)

SCHEMA_VERSION = 2  # W20F5: _anchor_candidate_surface fallback (ramped) for fp without clean_background_expected=True overlay
# Deterministic step — no prompt template. Stamp constant for config_hash
# stability across runs / resume.
PROMPT_VERSION = "1"


class BaseLocationDossierStep(StepRunner):
    def _config_hash(self) -> str:
        from app.core.config import settings

        # Only stamp the dossier selector when it is opt-in. With the
        # default OFF the step always returns not_applicable, so the hash
        # composition stays small and stable. The flag itself is included
        # so flipping it on / off invalidates the dossier checkpoint
        # cleanly.
        payload = {
            "background_mode": settings.background_mode,
            "floor_plan_prompt_version": settings.floor_plan_prompt_version,
            "base_location_dossier_enabled": bool(
                settings.base_location_dossier_enabled
            ),
            "schema_version": SCHEMA_VERSION,
            "prompt_version": PROMPT_VERSION,
        }
        return hashlib.sha256(
            json.dumps(payload, sort_keys=True).encode("utf-8")
        ).hexdigest()[:16]

    def _load_prev_checkpoint(
        self, step_id: str
    ) -> Optional[Dict[str, Any]]:
        from app.core.config import settings

        cp = (
            Path(settings.projects_dir)
            / self.project_id
            / "checkpoints"
            / "episodes"
            / self.episode_id
            / step_id
            / "manifest.json"
        )
        if cp.exists():
            try:
                return json.loads(cp.read_text(encoding="utf-8"))
            except Exception as exc:
                logger.warning(
                    "base_location_dossier: %s parse failed: %s",
                    step_id,
                    exc,
                )
        return None

    def _not_applicable(self) -> Dict[str, Any]:
        return {
            "applicable_count": 0,
            "completed_count": 0,
            "failed_count": 0,
            "schema_version": SCHEMA_VERSION,
            "config_hash": self._config_hash(),
            "data": {},
        }

    def _execute(self, mode: str = "resume") -> Dict[str, Any]:
        from app.core.config import settings

        if settings.background_mode not in {"on", "floor_plan_anchored"}:
            return self._not_applicable()
        if not bool(settings.base_location_dossier_enabled):
            return self._not_applicable()
        # v7/v8/v9 are schema-compatible with v6 (numbered_elements identical);
        # accept all. v5 still routes to not_applicable. SOT: fp_prompt_compat.
        from app.core.fp_prompt_compat import V6_COMPATIBLE_FP_PROMPT_VERSIONS
        if settings.floor_plan_prompt_version not in V6_COMPATIBLE_FP_PROMPT_VERSIONS:
            return self._not_applicable()

        fp_prompt_cp = self._load_prev_checkpoint("floor_plan_prompt")
        master_plan_cp = self._load_prev_checkpoint("background_master_plan")
        overlay_cp = self._load_prev_checkpoint("floor_plan_overlay_payload")
        fp_render_cp = self._load_prev_checkpoint("floor_plan_render")

        fp_prompt_data = (fp_prompt_cp or {}).get("data") or {}
        master_plan_data = (master_plan_cp or {}).get("data") or {}
        overlay_data = (overlay_cp or {}).get("data") or {}
        fp_render_data = (fp_render_cp or {}).get("data") or {}

        try:
            dossiers = build_dossiers(
                fp_prompt_data=fp_prompt_data,
                master_plan_data=master_plan_data,
                overlay_payload_data=overlay_data,
                fp_render_data=fp_render_data,
            )
        except BaseLocationDossierError as exc:
            logger.error("base_location_dossier: %s", exc)
            return {
                "applicable_count": 1,
                "completed_count": 0,
                "failed_count": 1,
                "schema_version": SCHEMA_VERSION,
                "config_hash": self._config_hash(),
                "data": {"error": str(exc)[:200], "dossiers": {}},
            }

        return {
            "applicable_count": 1,
            "completed_count": 1,
            "failed_count": 0,
            "schema_version": SCHEMA_VERSION,
            "config_hash": self._config_hash(),
            "data": {"dossiers": dossiers},
        }
