"""Deal Desk configuration models and loader."""

from __future__ import annotations

from pathlib import Path
from typing import Any, Literal

try:
    import yaml
except Exception as exc:  # pragma: no cover - handled at runtime
    yaml = None  # type: ignore
    _yaml_import_error = exc
else:
    _yaml_import_error = None

from pydantic import BaseModel, ConfigDict, Field, model_validator


class DealDeskConfigError(Exception):
    """Raised when Deal Desk config cannot be loaded or validated."""


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

    close_won_stage: str = "Closed Won"
    close_lost_stage: str = "Closed Lost"
    negotiation_stage: str = "Negotiation"


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

    name: str
    min_amount: float | None = None
    max_amount: float | None = None

    def matches(self, amount: float | None) -> bool:
        if amount is None:
            return False
        # Inclusive bounds: min_amount <= amount <= max_amount
        if self.min_amount is not None and amount < self.min_amount:
            return False
        if self.max_amount is not None and amount > self.max_amount:
            return False
        return True


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

    close_date_not_future_days: int = 7
    close_date_not_stale_days: int = 30
    contract_start_field: str | None = None
    contract_end_field: str | None = None
    contract_start_after_close: bool = True
    contract_end_after_start: bool = True


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

    amount_field: str = "Amount"
    term_months_field: str | None = None
    term_unit: str = "months"  # months | days | years
    tcv_field: str | None = None
    arr_field: str | None = None
    acv_field: str | None = None
    line_item_sobject: str | None = None
    amount_vs_line_items_tolerance_pct: float = 1.0
    amount_must_be_positive: bool = True
    amount_semantics: str = "tcv"  # tcv | acv
    rounding: str = "cents"  # cents | dollars


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

    slack_id: str | None = None
    fallback_channel: str | None = None


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

    enabled: bool = False
    sources: list[str] = Field(default_factory=list)
    min_attachments: int = 0
    block_close_if_missing: bool = False


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

    enabled: bool = True
    date_offset_days: int = 365
    term_default_months: int = 12
    stage_default: str = "Qualification"
    name_template: str = "{original_name} - Renewal {year}"
    copy_fields: list[str] = Field(default_factory=list)
    provenance_field: str | None = None
    provenance_description_marker: bool = True
    rollback_window_hours: int = 24


CertificationStepPolicyMode = Literal["auto", "approve", "skip"]


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

    mode: CertificationStepPolicyMode = "auto"


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

    close_progression: CertificationStepPolicyConfig = Field(default_factory=CertificationStepPolicyConfig)
    post_close_execution: CertificationStepPolicyConfig = Field(default_factory=CertificationStepPolicyConfig)


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

    trigger_key: str
    enabled: bool = True
    execution_order: int = 100
    on_failure: Literal["block", "escalate"] = "escalate"
    require_preview_before_live: bool = True
    preview_approved: bool = False


def _default_post_close_triggers() -> list["PostCloseTriggerConfig"]:
    return [
        PostCloseTriggerConfig(
            trigger_key="renewal_create_shell",
            enabled=True,
            execution_order=10,
            on_failure="escalate",
            require_preview_before_live=True,
            preview_approved=False,
        )
    ]


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

    triggers: list[PostCloseTriggerConfig] = Field(default_factory=_default_post_close_triggers)


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

    min_amount: float | None = None
    max_amount: float | None = None
    stage: str | None = None
    band: str | None = None


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

    role: str | None = None
    slack_id: str | None = None
    channel_id: str | None = None


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

    name: str
    match: ApprovalMatch = Field(default_factory=ApprovalMatch)
    approvers: list[ApproverConfig] = Field(default_factory=list)


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

    schema_version: str = "1"
    stages: StagesConfig = Field(default_factory=StagesConfig)
    size_bands: list[SizeBand] = Field(default_factory=list)
    required_fields: dict[str, dict[str, list[str]]] = Field(default_factory=dict)
    date_rules: DateRules = Field(default_factory=DateRules)
    amount_rules: AmountRules = Field(default_factory=AmountRules)
    roles: dict[str, RoleConfig] = Field(default_factory=dict)
    attachment_rules: AttachmentRules = Field(default_factory=AttachmentRules)
    certification_steps: CertificationStepPoliciesConfig = Field(default_factory=CertificationStepPoliciesConfig)
    renewal: RenewalConfig = Field(default_factory=RenewalConfig)
    post_close: PostCloseConfig = Field(default_factory=PostCloseConfig)
    approval_routing: list[ApprovalRoute] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_schema(self) -> "DealDeskConfig":
        if str(self.schema_version) not in ("1",):
            raise DealDeskConfigError(f"Unsupported schema_version: {self.schema_version}")
        return self

    def resolve_size_band(self, amount: float | None) -> str:
        for band in self.size_bands:
            if band.matches(amount):
                return band.name
        return "default"

    def required_fields_for(self, stage: str, band: str) -> list[str]:
        stage_rules = self.required_fields.get(stage, {}) or {}
        if band in stage_rules:
            return list(stage_rules.get(band) or [])
        return list(stage_rules.get("default") or [])

    def configured_fields(self) -> set[str]:
        fields: set[str] = set()
        for stage_rules in self.required_fields.values():
            if not isinstance(stage_rules, dict):
                continue
            for field_list in stage_rules.values():
                if isinstance(field_list, list):
                    fields.update(str(f) for f in field_list if f)
        if self.date_rules.contract_start_field:
            fields.add(self.date_rules.contract_start_field)
        if self.date_rules.contract_end_field:
            fields.add(self.date_rules.contract_end_field)
        amount_rules = self.amount_rules
        for name in (
            amount_rules.amount_field,
            amount_rules.term_months_field,
            amount_rules.tcv_field,
            amount_rules.arr_field,
            amount_rules.acv_field,
        ):
            if name:
                fields.add(name)
        if self.renewal.provenance_field:
            fields.add(self.renewal.provenance_field)
        for field in self.renewal.copy_fields:
            if field:
                fields.add(field)
        return fields


from g_gremlin.config import get_config_dir as _get_config_dir

DEFAULT_CONFIG_PATH = _get_config_dir() / "dealdesk" / "dealdesk.yaml"


def load_dealdesk_config(path: str | Path | None = None) -> DealDeskConfig:
    config_path = Path(path) if path else DEFAULT_CONFIG_PATH
    if not config_path.exists():
        raise DealDeskConfigError(f"Deal Desk config not found: {config_path}")
    if yaml is None:  # pragma: no cover
        raise DealDeskConfigError("PyYAML is required to load dealdesk config.") from _yaml_import_error
    try:
        with config_path.open("r", encoding="utf-8") as handle:
            raw = yaml.safe_load(handle) or {}
    except Exception as exc:
        raise DealDeskConfigError(f"Failed to parse dealdesk config: {exc}") from exc
    if not isinstance(raw, dict):
        raise DealDeskConfigError("Deal Desk config must be a mapping/object.")
    return DealDeskConfig.model_validate(raw)


__all__ = [
    "DealDeskConfig",
    "DealDeskConfigError",
    "load_dealdesk_config",
    "StagesConfig",
    "SizeBand",
    "DateRules",
    "AmountRules",
    "RoleConfig",
    "AttachmentRules",
    "CertificationStepPolicyMode",
    "CertificationStepPolicyConfig",
    "CertificationStepPoliciesConfig",
    "RenewalConfig",
    "PostCloseTriggerConfig",
    "PostCloseConfig",
    "ApprovalRoute",
]
