"""Admin read models for Deal Desk quote-intelligence review."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, computed_field

from .config import ApprovalRoute, DealDeskConfig, SizeBand
from .profiles import FieldMappingCandidate, QuoteEnvironmentProfileV1
from .store import DealDeskStore


_SEMANTIC_LABELS: dict[str, str] = {
    "amount": "Amount",
    "arr": "ARR",
    "discount_pct": "Discount %",
    "term_months": "Term Months",
    "payment_terms": "Payment Terms",
    "contract_start": "Contract Start",
    "contract_end": "Contract End",
    "legal_entity": "Legal Entity",
    "primary_contact": "Primary Contact",
    "pricebook": "Pricebook",
    "template_key": "Template",
    "quote_status": "Quote Status",
    "quote_type": "Quote Type",
}


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

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


class QuoteIntelligenceArtifactPathsV1(_QuoteIntelligenceModel):
    config_path: str | None = None
    quote_environment_profile_path: str | None = None


class QuoteIntelligenceFieldMappingV1(_QuoteIntelligenceModel):
    version: int = 1
    semantic_key: str
    label: str
    object_name: str
    field_name: str
    source_field: str
    confidence: float = 0.0
    rationale: str = ""
    confirmed: bool = False
    evidence_ids: list[str] = Field(default_factory=list)
    active_paths: list[str] = Field(default_factory=list)
    candidate_count: int = 1
    alternate_sources: list[str] = Field(default_factory=list)
    status: str = "discovered"

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


class QuoteIntelligenceRequiredFieldEntryV1(_QuoteIntelligenceModel):
    version: int = 1
    field_name: str
    status: str
    semantic_keys: list[str] = Field(default_factory=list)
    notes: list[str] = Field(default_factory=list)

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


class QuoteIntelligenceRequiredFieldCoverageV1(_QuoteIntelligenceModel):
    version: int = 1
    stage: str
    band: str
    selected: bool = False
    coverage_status: str = "missing"
    field_count: int = 0
    fields: list[QuoteIntelligenceRequiredFieldEntryV1] = Field(default_factory=list)

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


class QuoteIntelligenceApproverV1(_QuoteIntelligenceModel):
    version: int = 1
    role: str | None = None
    slack_id: str | None = None
    channel_id: str | None = None
    resolved_slack_id: str | None = None
    resolved_channel_id: str | None = None

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


class QuoteIntelligenceApprovalRouteEvaluationV1(_QuoteIntelligenceModel):
    version: int = 1
    route_name: str
    matched: bool = False
    matched_on: list[str] = Field(default_factory=list)
    mismatch_reasons: list[str] = Field(default_factory=list)
    approver_count: int = 0
    approvers: list[QuoteIntelligenceApproverV1] = Field(default_factory=list)

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


class QuoteIntelligenceApprovalPreviewV1(_QuoteIntelligenceModel):
    version: int = 1
    selected_stage: str
    selected_amount: float | None = None
    resolved_band: str = "default"
    route_count: int = 0
    matched_count: int = 0
    items: list[QuoteIntelligenceApprovalRouteEvaluationV1] = Field(default_factory=list)
    notes: list[str] = Field(default_factory=list)

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


class QuoteIntelligenceScenarioV1(_QuoteIntelligenceModel):
    version: int = 1
    scenario_key: str
    label: str
    description: str = ""
    stage: str
    amount: float | None = None
    resolved_band: str = "default"
    coverage_status: str = "missing"
    matched_route_count: int = 0
    route_count: int = 0
    selected: bool = False

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


class DealDeskQuoteIntelligenceResponseV1(_QuoteIntelligenceModel):
    version: int = 1
    status: str
    crm_org_id: str | None = None
    org_alias: str | None = None
    config_source: str
    config_path: str
    environment_profile_present: bool = False
    selected_stage: str
    selected_amount: float | None = None
    selected_band: str = "default"
    stage_options: list[str] = Field(default_factory=list)
    band_options: list[str] = Field(default_factory=list)
    field_mapping_count: int = 0
    field_mappings: list[QuoteIntelligenceFieldMappingV1] = Field(default_factory=list)
    scenarios: list[QuoteIntelligenceScenarioV1] = Field(default_factory=list)
    required_field_coverage: list[QuoteIntelligenceRequiredFieldCoverageV1] = Field(default_factory=list)
    approval_preview: QuoteIntelligenceApprovalPreviewV1
    artifact_paths: QuoteIntelligenceArtifactPathsV1 = Field(default_factory=QuoteIntelligenceArtifactPathsV1)

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


@dataclass(frozen=True)
class _ScenarioSeed:
    scenario_key: str
    label: str
    description: str
    stage: str
    amount: float | None


def load_dealdesk_quote_intelligence(
    state_dir: str | Path | None,
    *,
    crm_org_id: str | None = None,
    org_alias: str | None = None,
    stage: str | None = None,
    amount: float | None = None,
) -> DealDeskQuoteIntelligenceResponseV1:
    store = DealDeskStore(state_dir)
    config, config_source, config_path = store.load_effective_config_info(
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    environment_profile = store.load_quote_environment_profile(
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    selected_stage = str(stage or "").strip() or config.stages.close_won_stage
    selected_amount = amount if amount is not None else None
    selected_band = config.resolve_size_band(selected_amount)
    field_mappings = _build_field_mappings(
        config=config,
        environment_profile=environment_profile,
    )
    required_field_coverage = _build_required_field_coverage(
        config=config,
        environment_profile=environment_profile,
        selected_stage=selected_stage,
        selected_band=selected_band,
    )
    approval_preview = _build_approval_preview(
        config=config,
        selected_stage=selected_stage,
        selected_amount=selected_amount,
        selected_band=selected_band,
    )
    scenarios = _build_scenarios(
        config=config,
        environment_profile=environment_profile,
        selected_stage=selected_stage,
        selected_amount=selected_amount,
    )
    status = "ok" if environment_profile is not None else "partial"
    return DealDeskQuoteIntelligenceResponseV1(
        status=status,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        config_source=config_source,
        config_path=config_path,
        environment_profile_present=environment_profile is not None,
        selected_stage=selected_stage,
        selected_amount=selected_amount,
        selected_band=selected_band,
        stage_options=_stage_options(config),
        band_options=_band_options(config),
        field_mapping_count=len(field_mappings),
        field_mappings=field_mappings,
        scenarios=scenarios,
        required_field_coverage=required_field_coverage,
        approval_preview=approval_preview,
        artifact_paths=QuoteIntelligenceArtifactPathsV1(
            config_path=config_path,
            quote_environment_profile_path=str(
                store.quote_environment_profile_path(
                    crm_org_id=crm_org_id,
                    org_alias=org_alias,
                )
            ),
        ),
    )


def _build_field_mappings(
    *,
    config: DealDeskConfig,
    environment_profile: QuoteEnvironmentProfileV1 | None,
) -> list[QuoteIntelligenceFieldMappingV1]:
    if environment_profile is None:
        return []
    active_paths_by_field = _active_paths_by_field(config)
    grouped: dict[str, list[FieldMappingCandidate]] = {}
    for item in environment_profile.field_mappings:
        grouped.setdefault(item.semantic_key, []).append(item)

    items: list[QuoteIntelligenceFieldMappingV1] = []
    for semantic_key in sorted(grouped.keys()):
        candidates = sorted(grouped[semantic_key], key=lambda value: (-value.confidence, value.object_name, value.field_name))
        primary = candidates[0]
        active_paths = active_paths_by_field.get(primary.field_name, [])
        items.append(
            QuoteIntelligenceFieldMappingV1(
                semantic_key=semantic_key,
                label=_semantic_label(semantic_key),
                object_name=primary.object_name,
                field_name=primary.field_name,
                source_field=f"{primary.object_name}.{primary.field_name}",
                confidence=primary.confidence,
                rationale=primary.rationale,
                confirmed=bool(primary.confirmed),
                evidence_ids=list(primary.evidence_ids),
                active_paths=active_paths,
                candidate_count=len(candidates),
                alternate_sources=[f"{item.object_name}.{item.field_name}" for item in candidates[1:4]],
                status="active" if active_paths else "discovered",
            )
        )
    return items


def _build_required_field_coverage(
    *,
    config: DealDeskConfig,
    environment_profile: QuoteEnvironmentProfileV1 | None,
    selected_stage: str,
    selected_band: str,
) -> list[QuoteIntelligenceRequiredFieldCoverageV1]:
    mapped_semantics_by_field: dict[str, set[str]] = {}
    if environment_profile is not None:
        for item in environment_profile.field_mappings:
            mapped_semantics_by_field.setdefault(item.field_name, set()).add(item.semantic_key)

    coverage_rows: list[QuoteIntelligenceRequiredFieldCoverageV1] = []
    selected_rule_band = _effective_stage_band(config, selected_stage, selected_band)
    stage_rules = config.required_fields or {}
    for stage_name in _stage_options(config):
        band_rules = stage_rules.get(stage_name, {}) if isinstance(stage_rules.get(stage_name), dict) else {}
        normalized_band_rules = dict(band_rules or {})
        if not normalized_band_rules:
            normalized_band_rules = {"default": []}
        for band_name in sorted(normalized_band_rules.keys(), key=lambda item: (item != "default", str(item))):
            field_names = [str(value).strip() for value in normalized_band_rules.get(band_name, []) if str(value).strip()]
            entries: list[QuoteIntelligenceRequiredFieldEntryV1] = []
            mapped_count = 0
            for field_name in field_names:
                semantic_keys = sorted(mapped_semantics_by_field.get(field_name, set()))
                if semantic_keys:
                    mapped_count += 1
                    status = "mapped"
                    notes = [f"Mapped semantic(s): {', '.join(_semantic_label(item) for item in semantic_keys)}."]
                else:
                    status = "configured_only"
                    notes = ["Field is required by config but was not surfaced in refreshed semantic mappings."]
                entries.append(
                    QuoteIntelligenceRequiredFieldEntryV1(
                        field_name=field_name,
                        status=status,
                        semantic_keys=semantic_keys,
                        notes=notes,
                    )
                )
            if not entries:
                coverage_status = "missing"
            elif mapped_count == len(entries):
                coverage_status = "mapped"
            else:
                coverage_status = "partial"
            coverage_rows.append(
                QuoteIntelligenceRequiredFieldCoverageV1(
                    stage=stage_name,
                    band=str(band_name),
                    selected=stage_name == selected_stage and str(band_name) == selected_rule_band,
                    coverage_status=coverage_status,
                    field_count=len(entries),
                    fields=entries,
                )
            )
    return coverage_rows


def _build_approval_preview(
    *,
    config: DealDeskConfig,
    selected_stage: str,
    selected_amount: float | None,
    selected_band: str,
) -> QuoteIntelligenceApprovalPreviewV1:
    items = [
        _evaluate_approval_route(
            route=item,
            config=config,
            selected_stage=selected_stage,
            selected_amount=selected_amount,
            selected_band=selected_band,
        )
        for item in config.approval_routing
    ]
    notes: list[str] = []
    if selected_amount is None:
        notes.append("No amount was provided, so approval routes with amount thresholds will not match.")
    notes.append(f"Resolved size band for this preview: {selected_band}.")
    if not items:
        notes.append("No approval-routing rules are configured for this Deal Desk scope.")
    return QuoteIntelligenceApprovalPreviewV1(
        selected_stage=selected_stage,
        selected_amount=selected_amount,
        resolved_band=selected_band,
        route_count=len(items),
        matched_count=sum(1 for item in items if item.matched),
        items=items,
        notes=notes,
    )


def _build_scenarios(
    *,
    config: DealDeskConfig,
    environment_profile: QuoteEnvironmentProfileV1 | None,
    selected_stage: str,
    selected_amount: float | None,
) -> list[QuoteIntelligenceScenarioV1]:
    scenarios: list[QuoteIntelligenceScenarioV1] = []
    for seed in _scenario_seeds(config):
        resolved_band = config.resolve_size_band(seed.amount)
        coverage_rows = _build_required_field_coverage(
            config=config,
            environment_profile=environment_profile,
            selected_stage=seed.stage,
            selected_band=resolved_band,
        )
        selected_coverage = next((item for item in coverage_rows if item.selected), None)
        approval_preview = _build_approval_preview(
            config=config,
            selected_stage=seed.stage,
            selected_amount=seed.amount,
            selected_band=resolved_band,
        )
        scenarios.append(
            QuoteIntelligenceScenarioV1(
                scenario_key=seed.scenario_key,
                label=seed.label,
                description=seed.description,
                stage=seed.stage,
                amount=seed.amount,
                resolved_band=resolved_band,
                coverage_status=selected_coverage.coverage_status if selected_coverage is not None else "missing",
                matched_route_count=approval_preview.matched_count,
                route_count=approval_preview.route_count,
                selected=_scenario_matches_preview(
                    seed,
                    selected_stage=selected_stage,
                    selected_amount=selected_amount,
                ),
            )
        )
    return scenarios


def _scenario_seeds(config: DealDeskConfig) -> list[_ScenarioSeed]:
    close_won_stage = str(config.stages.close_won_stage or "").strip() or "Closed Won"
    negotiation_stage = str(config.stages.negotiation_stage or "").strip() or close_won_stage
    default_amount = _representative_default_amount(config)
    seeds: list[_ScenarioSeed] = [
        _ScenarioSeed(
            scenario_key="negotiation_baseline",
            label="Negotiation Baseline",
            description="Baseline negotiation review before close-specific routing applies.",
            stage=negotiation_stage,
            amount=default_amount,
        ),
        _ScenarioSeed(
            scenario_key="close_baseline",
            label="Close Baseline",
            description="Baseline closed-won review before named size-band routing kicks in.",
            stage=close_won_stage,
            amount=default_amount,
        ),
    ]
    for band in config.size_bands[:2]:
        band_name = str(band.name or "").strip()
        if not band_name:
            continue
        seeds.append(
            _ScenarioSeed(
                scenario_key=f"close_{_slug(band_name)}",
                label=f"{_pretty_label(band_name)} Close",
                description=f"Closed-won review for the {band_name} size band.",
                stage=close_won_stage,
                amount=_representative_amount_for_band(config, band_name),
            )
        )
    for route in config.approval_routing[:2]:
        route_name = str(route.name or "").strip() or "approval_route"
        route_stage = str(route.match.stage or "").strip() or close_won_stage
        seeds.append(
            _ScenarioSeed(
                scenario_key=f"route_{_slug(route_name)}",
                label=f"{route_name} Trigger",
                description="Scenario shaped to trigger this approval route when the config allows it.",
                stage=route_stage,
                amount=_representative_amount_for_route(config, route),
            )
        )

    deduped: list[_ScenarioSeed] = []
    seen: set[tuple[str, int]] = set()
    for seed in seeds:
        key = (seed.stage.strip().lower(), _amount_bucket(seed.amount))
        if key in seen:
            continue
        seen.add(key)
        deduped.append(seed)
        if len(deduped) >= 6:
            break
    return deduped


def _evaluate_approval_route(
    *,
    route: ApprovalRoute,
    config: DealDeskConfig,
    selected_stage: str,
    selected_amount: float | None,
    selected_band: str,
) -> QuoteIntelligenceApprovalRouteEvaluationV1:
    matched_on: list[str] = []
    mismatch_reasons: list[str] = []
    match = route.match
    has_criteria = False

    if str(match.stage or "").strip():
        has_criteria = True
        if str(match.stage).strip().lower() == str(selected_stage).strip().lower():
            matched_on.append(f"Stage = {match.stage}")
        else:
            mismatch_reasons.append(f"Stage must be {match.stage}.")
    if str(match.band or "").strip():
        has_criteria = True
        if str(match.band).strip().lower() == str(selected_band).strip().lower():
            matched_on.append(f"Band = {match.band}")
        else:
            mismatch_reasons.append(f"Band must be {match.band}.")
    if match.min_amount is not None:
        has_criteria = True
        if selected_amount is None:
            mismatch_reasons.append(f"Amount must be at least {match.min_amount:,.2f}.")
        elif selected_amount >= float(match.min_amount):
            matched_on.append(f"Amount >= {match.min_amount:,.2f}")
        else:
            mismatch_reasons.append(f"Amount must be at least {match.min_amount:,.2f}.")
    if match.max_amount is not None:
        has_criteria = True
        if selected_amount is None:
            mismatch_reasons.append(f"Amount must be no more than {match.max_amount:,.2f}.")
        elif selected_amount <= float(match.max_amount):
            matched_on.append(f"Amount <= {match.max_amount:,.2f}")
        else:
            mismatch_reasons.append(f"Amount must be no more than {match.max_amount:,.2f}.")

    if not has_criteria:
        matched_on.append("No explicit match criteria; route applies to all deal shapes.")

    approvers = [
        QuoteIntelligenceApproverV1(
            role=str(item.role or "").strip() or None,
            slack_id=str(item.slack_id or "").strip() or None,
            channel_id=str(item.channel_id or "").strip() or None,
            resolved_slack_id=_resolved_approver_slack_id(config, item.role, item.slack_id),
            resolved_channel_id=_resolved_approver_channel_id(config, item.role, item.channel_id),
        )
        for item in route.approvers
    ]
    return QuoteIntelligenceApprovalRouteEvaluationV1(
        route_name=route.name,
        matched=not mismatch_reasons,
        matched_on=matched_on,
        mismatch_reasons=mismatch_reasons,
        approver_count=len(approvers),
        approvers=approvers,
    )


def _representative_default_amount(config: DealDeskConfig) -> float:
    min_candidates = [float(item.min_amount) for item in config.size_bands if item.min_amount is not None]
    if not min_candidates:
        return 50_000.0
    smallest_min = min(min_candidates)
    if smallest_min >= 100_000:
        return 50_000.0
    if smallest_min > 1_000:
        return _round_amount(max(1_000.0, smallest_min * 0.5))
    if smallest_min > 1:
        return float(int(smallest_min) - 1)
    return 1_000.0


def _representative_amount_for_band(config: DealDeskConfig, band_name: str) -> float:
    band = _band_by_name(config, band_name)
    if band is None:
        return _representative_default_amount(config)
    return _amount_for_bounds(
        min_amount=band.min_amount,
        max_amount=band.max_amount,
        fallback=_representative_default_amount(config),
        prefer_threshold=False,
    )


def _representative_amount_for_route(config: DealDeskConfig, route: ApprovalRoute) -> float:
    band = _band_by_name(config, route.match.band)
    min_amounts = [float(value) for value in (route.match.min_amount, band.min_amount if band is not None else None) if value is not None]
    max_amounts = [float(value) for value in (route.match.max_amount, band.max_amount if band is not None else None) if value is not None]
    min_amount = max(min_amounts) if min_amounts else None
    max_amount = min(max_amounts) if max_amounts else None
    return _amount_for_bounds(
        min_amount=min_amount,
        max_amount=max_amount,
        fallback=_representative_default_amount(config),
        prefer_threshold=True,
    )


def _amount_for_bounds(
    *,
    min_amount: float | None,
    max_amount: float | None,
    fallback: float,
    prefer_threshold: bool,
) -> float:
    if min_amount is not None and max_amount is not None and min_amount <= max_amount:
        if prefer_threshold:
            return _round_amount(min_amount)
        return _round_amount((min_amount + max_amount) / 2.0)
    if min_amount is not None and max_amount is not None and min_amount > max_amount:
        return _round_amount(min_amount)
    if min_amount is not None:
        if prefer_threshold:
            return _round_amount(min_amount)
        return _round_amount(min_amount + max(10_000.0, min_amount * 0.25))
    if max_amount is not None:
        if prefer_threshold:
            return _round_amount(max(1_000.0, max_amount * 0.75))
        return _round_amount(max(1_000.0, max_amount * 0.75))
    return _round_amount(fallback)


def _round_amount(value: float) -> float:
    if value >= 100_000:
        return float(round(value / 5_000.0) * 5_000)
    if value >= 10_000:
        return float(round(value / 1_000.0) * 1_000)
    if value >= 1_000:
        return float(round(value / 100.0) * 100)
    return float(round(value, 2))


def _band_by_name(config: DealDeskConfig, band_name: str | None) -> SizeBand | None:
    normalized_band = str(band_name or "").strip().lower()
    if not normalized_band:
        return None
    for item in config.size_bands:
        if str(item.name or "").strip().lower() == normalized_band:
            return item
    return None


def _scenario_matches_preview(
    seed: _ScenarioSeed,
    *,
    selected_stage: str,
    selected_amount: float | None,
) -> bool:
    if seed.stage.strip().lower() != str(selected_stage or "").strip().lower():
        return False
    if seed.amount is None and selected_amount is None:
        return True
    if seed.amount is None or selected_amount is None:
        return False
    return abs(float(seed.amount) - float(selected_amount)) < 0.01


def _amount_bucket(value: float | None) -> int:
    if value is None:
        return -1
    return int(round(float(value) * 100))


def _pretty_label(value: str) -> str:
    return str(value or "").replace("_", " ").strip().title() or "Unknown"


def _slug(value: str) -> str:
    normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(value or "").strip())
    slug = "_".join(part for part in normalized.split("_") if part)
    return slug or "scenario"


def _active_paths_by_field(config: DealDeskConfig) -> dict[str, list[str]]:
    paths: dict[str, list[str]] = {}

    def remember(field_name: str | None, path: str) -> None:
        normalized = str(field_name or "").strip()
        if not normalized:
            return
        paths.setdefault(normalized, []).append(path)

    remember(config.amount_rules.amount_field, "amount_rules.amount_field")
    remember(config.amount_rules.arr_field, "amount_rules.arr_field")
    remember(config.amount_rules.acv_field, "amount_rules.acv_field")
    remember(config.amount_rules.tcv_field, "amount_rules.tcv_field")
    remember(config.amount_rules.term_months_field, "amount_rules.term_months_field")
    remember(config.date_rules.contract_start_field, "date_rules.contract_start_field")
    remember(config.date_rules.contract_end_field, "date_rules.contract_end_field")
    for stage_name, band_rules in (config.required_fields or {}).items():
        if not isinstance(band_rules, dict):
            continue
        for band_name, field_names in band_rules.items():
            if not isinstance(field_names, list):
                continue
            for field_name in field_names:
                remember(str(field_name or "").strip(), f"required_fields.{stage_name}.{band_name}")
    return {key: sorted(dict.fromkeys(value)) for key, value in paths.items()}


def _stage_options(config: DealDeskConfig) -> list[str]:
    ordered = [
        config.stages.negotiation_stage,
        config.stages.close_won_stage,
        config.stages.close_lost_stage,
    ]
    for stage_name in (config.required_fields or {}).keys():
        ordered.append(str(stage_name))
    result: list[str] = []
    seen: set[str] = set()
    for item in ordered:
        normalized = str(item or "").strip()
        if not normalized or normalized.lower() in seen:
            continue
        seen.add(normalized.lower())
        result.append(normalized)
    return result


def _band_options(config: DealDeskConfig) -> list[str]:
    names = ["default"]
    names.extend(str(item.name).strip() for item in config.size_bands if str(item.name).strip())
    result: list[str] = []
    seen: set[str] = set()
    for item in names:
        if item.lower() in seen:
            continue
        seen.add(item.lower())
        result.append(item)
    return result


def _effective_stage_band(config: DealDeskConfig, stage: str, band: str) -> str:
    stage_rules = config.required_fields.get(stage, {}) if isinstance(config.required_fields.get(stage), dict) else {}
    normalized_band = str(band or "default").strip() or "default"
    if normalized_band in stage_rules:
        return normalized_band
    return "default"


def _semantic_label(value: str) -> str:
    normalized = str(value or "").strip()
    if normalized in _SEMANTIC_LABELS:
        return _SEMANTIC_LABELS[normalized]
    return normalized.replace("_", " ").title() or "Unknown"


def _resolved_approver_slack_id(config: DealDeskConfig, role: str | None, explicit: str | None) -> str | None:
    normalized_explicit = str(explicit or "").strip() or None
    if normalized_explicit is not None:
        return normalized_explicit
    normalized_role = str(role or "").strip()
    if not normalized_role:
        return None
    role_config = config.roles.get(normalized_role)
    return str(role_config.slack_id or "").strip() or None if role_config is not None else None


def _resolved_approver_channel_id(config: DealDeskConfig, role: str | None, explicit: str | None) -> str | None:
    normalized_explicit = str(explicit or "").strip() or None
    if normalized_explicit is not None:
        return normalized_explicit
    normalized_role = str(role or "").strip()
    if not normalized_role:
        return None
    role_config = config.roles.get(normalized_role)
    return str(role_config.fallback_channel or "").strip() or None if role_config is not None else None


__all__ = [
    "DealDeskQuoteIntelligenceResponseV1",
    "QuoteIntelligenceApprovalPreviewV1",
    "QuoteIntelligenceApprovalRouteEvaluationV1",
    "QuoteIntelligenceApproverV1",
    "QuoteIntelligenceArtifactPathsV1",
    "QuoteIntelligenceFieldMappingV1",
    "QuoteIntelligenceRequiredFieldCoverageV1",
    "QuoteIntelligenceRequiredFieldEntryV1",
    "QuoteIntelligenceScenarioV1",
    "load_dealdesk_quote_intelligence",
]
