"""Governed promotion helpers for capability-refresh draft artifacts."""

from __future__ import annotations

import hashlib
import json
import secrets
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, computed_field

from .capability_refresh import DealDeskCapabilityRefreshReceiptV1
from .store import DealDeskStore


class _CapabilityPromotionModel(BaseModel):
    model_config = ConfigDict(extra="ignore")

    def to_dict(self) -> dict[str, Any]:
        return self.model_dump(exclude_none=False, by_alias=True)


class DealDeskCapabilityDraftPromotionChangeV1(_CapabilityPromotionModel):
    path: str
    before: Any = None
    after: Any = None


class DealDeskCapabilityDraftPromotionPreviewV1(_CapabilityPromotionModel):
    version: int = 1
    available: bool = False
    refresh_receipt_id: str | None = None
    refresh_receipt_path: str | None = None
    draft_path: str | None = None
    generated_config_exists: bool = False
    config_source_before: str | None = None
    config_path_before: str | None = None
    change_count: int = 0
    changed_paths: list[str] = Field(default_factory=list)
    already_active: bool = False
    notes: list[str] = Field(default_factory=list)

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "DealDeskCapabilityDraftPromotionPreview/v1"


class DealDeskCapabilityDraftPromotionReceiptV1(_CapabilityPromotionModel):
    version: int = 1
    receipt_id: str
    receipt_path: str
    created_at: str
    actor_id: str
    actor_email: str | None = None
    action: str = "promote_capability_draft"
    target_kind: str = "capability_draft_promotion"
    crm_org_id: str | None = None
    org_alias: str | None = None
    source_refresh_receipt_id: str
    source_refresh_receipt_path: str
    source_draft_path: str
    source_draft_hash: str
    config_source_before: str
    config_path_before: str
    config_path_after: str
    plan_summary: str
    risk_summary: str
    apply_summary: str
    before_config: dict[str, Any]
    after_config: dict[str, Any]
    changes: list[DealDeskCapabilityDraftPromotionChangeV1] = Field(default_factory=list)
    notes: list[str] = Field(default_factory=list)

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "DealDeskCapabilityDraftPromotionReceipt/v1"


class DealDeskCapabilityDraftPromotionReceiptSummaryV1(_CapabilityPromotionModel):
    version: int = 1
    receipt_id: str
    receipt_path: str
    created_at: str
    actor_id: str
    actor_email: str | None = None
    crm_org_id: str | None = None
    org_alias: str | None = None
    source_refresh_receipt_id: str
    source_draft_path: str
    config_source_before: str
    config_path_before: str
    config_path_after: str
    change_count: int = 0
    changed_paths_preview: list[str] = Field(default_factory=list)
    notes: list[str] = Field(default_factory=list)

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "DealDeskCapabilityDraftPromotionReceiptSummary/v1"


class PromoteDealDeskCapabilityDraftRequestV1(_CapabilityPromotionModel):
    version: int = 1
    crm_org_id: str | None = None
    org_alias: str | None = None
    refresh_receipt_id: str

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "PromoteDealDeskCapabilityDraftRequest/v1"


class PromoteDealDeskCapabilityDraftResultV1(_CapabilityPromotionModel):
    version: int = 1
    status: str
    message: str
    receipt: DealDeskCapabilityDraftPromotionReceiptV1 | None = None

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "PromoteDealDeskCapabilityDraftResult/v1"


def build_capability_draft_promotion_preview(
    state_dir: str | Path | None,
    *,
    crm_org_id: str | None = None,
    org_alias: str | None = None,
    refresh_receipt: DealDeskCapabilityRefreshReceiptV1 | None = None,
) -> DealDeskCapabilityDraftPromotionPreviewV1 | None:
    if refresh_receipt is None:
        return None
    draft_path = str(refresh_receipt.draft_config_path_after or "").strip()
    if not draft_path:
        return DealDeskCapabilityDraftPromotionPreviewV1(
            available=False,
            refresh_receipt_id=refresh_receipt.receipt_id,
            refresh_receipt_path=refresh_receipt.receipt_path,
            draft_path=None,
            notes=["The selected capability refresh did not produce a promotable draft artifact."],
        )

    store = DealDeskStore(state_dir)
    draft = store.load_capability_refresh_draft(draft_path=draft_path)
    if draft is None:
        return DealDeskCapabilityDraftPromotionPreviewV1(
            available=False,
            refresh_receipt_id=refresh_receipt.receipt_id,
            refresh_receipt_path=refresh_receipt.receipt_path,
            draft_path=draft_path,
            notes=["The draft artifact referenced by the capability refresh receipt is missing."],
        )

    generated_config = store.load_generated_config(
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    before_config, config_source_before, config_path_before = store.load_effective_config_info(
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    changes = _config_diff(
        before_config.model_dump(exclude_none=False),
        draft.model_dump(exclude_none=False),
    )
    already_active = generated_config is not None and not changes
    notes: list[str] = []
    if generated_config is None:
        notes.append("No generated config exists for this scope yet; promotion will create one from the draft artifact.")
    elif already_active:
        notes.append("The current generated config already matches this draft artifact.")
    else:
        notes.append("Promotion replaces the current generated config with the full capability-refresh draft.")
        notes.append("Operator-tuned trigger, step, and field settings may change because promotion applies the full draft artifact.")

    return DealDeskCapabilityDraftPromotionPreviewV1(
        available=True,
        refresh_receipt_id=refresh_receipt.receipt_id,
        refresh_receipt_path=refresh_receipt.receipt_path,
        draft_path=draft_path,
        generated_config_exists=generated_config is not None,
        config_source_before=config_source_before,
        config_path_before=config_path_before,
        change_count=len(changes),
        changed_paths=[item.path for item in changes],
        already_active=already_active,
        notes=notes,
    )


def promote_dealdesk_capability_draft(
    state_dir: str | Path | None,
    *,
    request: PromoteDealDeskCapabilityDraftRequestV1,
    actor_id: str,
    actor_email: str | None = None,
) -> PromoteDealDeskCapabilityDraftResultV1:
    store = DealDeskStore(state_dir)
    normalized_refresh_receipt_id = str(request.refresh_receipt_id or "").strip()
    normalized_crm_org_id = str(request.crm_org_id or "").strip() or None
    normalized_org_alias = str(request.org_alias or "").strip() or None

    if not normalized_refresh_receipt_id:
        raise RuntimeError("refresh_receipt_id is required for capability draft promotion.")
    if normalized_crm_org_id is None and normalized_org_alias is None:
        raise RuntimeError("crm_org_id or org_alias is required for capability draft promotion.")

    refresh_receipt = store.load_capability_refresh_receipt(
        receipt_id=normalized_refresh_receipt_id,
        crm_org_id=normalized_crm_org_id,
        org_alias=normalized_org_alias,
    )
    if refresh_receipt is None:
        raise KeyError(f"Capability refresh receipt {normalized_refresh_receipt_id} was not found.")

    draft_path = str(refresh_receipt.draft_config_path_after or "").strip()
    if not draft_path:
        raise RuntimeError(
            f"Capability refresh receipt {normalized_refresh_receipt_id} does not include a promotable draft artifact."
        )
    draft = store.load_capability_refresh_draft(draft_path=draft_path)
    if draft is None:
        raise RuntimeError(f"Capability refresh draft {draft_path} was not found.")

    generated_config = store.load_generated_config(
        crm_org_id=normalized_crm_org_id,
        org_alias=normalized_org_alias,
    )
    before_config, config_source_before, config_path_before = store.load_effective_config_info(
        crm_org_id=normalized_crm_org_id,
        org_alias=normalized_org_alias,
    )
    before_payload = before_config.model_dump(exclude_none=False)
    after_payload = draft.model_dump(exclude_none=False)
    changes = _config_diff(before_payload, after_payload)

    if generated_config is not None and not changes:
        existing_receipt = store.find_latest_capability_draft_promotion_receipt(
            crm_org_id=normalized_crm_org_id,
            org_alias=normalized_org_alias,
            source_refresh_receipt_id=normalized_refresh_receipt_id,
        )
        message = (
            f"Capability draft {normalized_refresh_receipt_id} is already active for this Deal Desk scope."
        )
        if existing_receipt is not None:
            message = f"{message} Returning the latest promotion receipt."
        return PromoteDealDeskCapabilityDraftResultV1(
            status="noop",
            message=message,
            receipt=existing_receipt,
        )

    after_config_path = store.save_generated_config(
        draft,
        crm_org_id=normalized_crm_org_id,
        org_alias=normalized_org_alias,
    )
    created_at = datetime.now(timezone.utc).isoformat()
    receipt_id = f"ddcapcfg_{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')}_{secrets.token_hex(4)}"
    receipt_path = store.capability_draft_promotion_receipt_path(
        receipt_id=receipt_id,
        crm_org_id=normalized_crm_org_id,
        org_alias=normalized_org_alias,
    )
    notes: list[str] = []
    if generated_config is None:
        notes.append("No generated config existed before promotion; this action created one from the selected draft artifact.")
    else:
        notes.append("Promotion replaced the current generated config with the full capability-refresh draft artifact.")
        notes.append("Operator-tuned values may change because capability draft promotion applies the full stored draft, not a field-level merge.")

    receipt = DealDeskCapabilityDraftPromotionReceiptV1(
        receipt_id=receipt_id,
        receipt_path=str(receipt_path),
        created_at=created_at,
        actor_id=str(actor_id or "").strip() or "unknown",
        actor_email=str(actor_email or "").strip() or None,
        crm_org_id=normalized_crm_org_id,
        org_alias=normalized_org_alias,
        source_refresh_receipt_id=normalized_refresh_receipt_id,
        source_refresh_receipt_path=refresh_receipt.receipt_path,
        source_draft_path=draft_path,
        source_draft_hash=_stable_payload_hash(after_payload),
        config_source_before=config_source_before,
        config_path_before=config_path_before,
        config_path_after=str(after_config_path),
        plan_summary=(
            f"Promote capability refresh draft {draft_path} from refresh receipt "
            f"{normalized_refresh_receipt_id} into the generated Deal Desk config for this scope."
        ),
        risk_summary=(
            "This writes the full stored draft into generated config. "
            + (
                "No generated config existed yet, so promotion will create one."
                if generated_config is None
                else "Existing operator-tuned values may change because this is a full-draft replacement."
            )
            + (
                f" Changed paths: {_summarize_paths([item.path for item in changes])}."
                if changes
                else ""
            )
        ),
        apply_summary=(
            f"Saved the promoted draft to {after_config_path} and persisted a capability draft promotion receipt."
        ),
        before_config=before_payload,
        after_config=after_payload,
        changes=changes,
        notes=notes,
    )
    store.save_capability_draft_promotion_receipt(
        receipt,
        crm_org_id=normalized_crm_org_id,
        org_alias=normalized_org_alias,
    )
    return PromoteDealDeskCapabilityDraftResultV1(
        status="applied",
        message=(
            f"Promoted capability draft {normalized_refresh_receipt_id} into the generated Deal Desk config."
        ),
        receipt=receipt,
    )


def summarize_capability_draft_promotion_receipt(
    receipt: DealDeskCapabilityDraftPromotionReceiptV1 | None,
) -> DealDeskCapabilityDraftPromotionReceiptSummaryV1 | None:
    if receipt is None:
        return None
    return DealDeskCapabilityDraftPromotionReceiptSummaryV1(
        receipt_id=receipt.receipt_id,
        receipt_path=receipt.receipt_path,
        created_at=receipt.created_at,
        actor_id=receipt.actor_id,
        actor_email=receipt.actor_email,
        crm_org_id=receipt.crm_org_id,
        org_alias=receipt.org_alias,
        source_refresh_receipt_id=receipt.source_refresh_receipt_id,
        source_draft_path=receipt.source_draft_path,
        config_source_before=receipt.config_source_before,
        config_path_before=receipt.config_path_before,
        config_path_after=receipt.config_path_after,
        change_count=len(receipt.changes),
        changed_paths_preview=[item.path for item in receipt.changes[:8]],
        notes=list(receipt.notes),
    )


def _config_diff(before: Any, after: Any, *, path: str = "") -> list[DealDeskCapabilityDraftPromotionChangeV1]:
    if isinstance(before, dict) and isinstance(after, dict):
        changes: list[DealDeskCapabilityDraftPromotionChangeV1] = []
        for key in sorted(set(before.keys()) | set(after.keys())):
            child_path = f"{path}.{key}" if path else str(key)
            changes.extend(_config_diff(before.get(key), after.get(key), path=child_path))
        return changes
    if isinstance(before, list) and isinstance(after, list):
        changes: list[DealDeskCapabilityDraftPromotionChangeV1] = []
        for index in range(max(len(before), len(after))):
            child_path = f"{path}[{index}]"
            before_value = before[index] if index < len(before) else None
            after_value = after[index] if index < len(after) else None
            changes.extend(_config_diff(before_value, after_value, path=child_path))
        return changes
    if before == after:
        return []
    return [DealDeskCapabilityDraftPromotionChangeV1(path=path or "$", before=before, after=after)]


def _stable_payload_hash(payload: dict[str, Any]) -> str:
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def _summarize_paths(paths: list[str], *, limit: int = 5) -> str:
    trimmed = [item for item in paths if item][: max(1, limit)]
    if not trimmed:
        return "none"
    if len(paths) <= len(trimmed):
        return ", ".join(trimmed)
    return f"{', '.join(trimmed)}, and {len(paths) - len(trimmed)} more"


__all__ = [
    "DealDeskCapabilityDraftPromotionChangeV1",
    "DealDeskCapabilityDraftPromotionPreviewV1",
    "DealDeskCapabilityDraftPromotionReceiptSummaryV1",
    "DealDeskCapabilityDraftPromotionReceiptV1",
    "PromoteDealDeskCapabilityDraftRequestV1",
    "PromoteDealDeskCapabilityDraftResultV1",
    "build_capability_draft_promotion_preview",
    "promote_dealdesk_capability_draft",
    "summarize_capability_draft_promotion_receipt",
]
