"""Deal Desk close verification scout."""

from __future__ import annotations

from datetime import date, datetime, timedelta, timezone
from typing import Any, Dict, List, Tuple

from g_gremlin.dealdesk.config import DealDeskConfig, load_dealdesk_config

from .base import BaseScoutCheck, PreflightResult
from ..crm_provider import Filter, FilterExpr, FilterOp
from ..models import EvidenceRow, RecommendedAction


def _utc_today() -> date:
    return datetime.now(timezone.utc).date()


def _parse_date(value: Any) -> date | None:
    if not value:
        return None
    if isinstance(value, date) and not isinstance(value, datetime):
        return value
    if isinstance(value, datetime):
        return value.date()
    text = str(value)
    for fmt in ("%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"):
        try:
            return datetime.strptime(text, fmt).date()
        except Exception:
            continue
    try:
        return datetime.fromisoformat(text.replace("Z", "+00:00")).date()
    except Exception:
        return None


def _to_float(value: Any) -> float | None:
    if value is None or value == "":
        return None
    try:
        return float(value)
    except Exception:
        return None


class DealDeskCloseVerificationCheck(BaseScoutCheck):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.config_path = self.params.get("config_path") or None
        self._config: DealDeskConfig | None = None
        self._available_fields: set[str] = set()
        self._permission_blocked_fields: list[str] = []
        self._field_available_map: dict[str, bool] = {}

    def preflight(self) -> PreflightResult:
        try:
            self._config = load_dealdesk_config(self.config_path)
        except Exception as exc:
            return PreflightResult(ok=False, unsupported=True, reason=str(exc))

        if not self.provider.supports_object("Opportunity"):
            return PreflightResult(ok=False, unsupported=True, reason="Unsupported object: Opportunity")

        try:
            desc = self.provider.describe("Opportunity")
        except Exception as exc:
            return PreflightResult(ok=False, unsupported=True, reason=str(exc))

        fields = desc.get("result", {}).get("fields", []) if isinstance(desc, dict) else []
        available: dict[str, dict[str, Any]] = {}
        for field in fields or []:
            name = field.get("name")
            if name:
                available[str(name).lower()] = field

        self._available_fields = {name for name, meta in available.items() if meta.get("accessible", True)}

        for field in sorted(self._config.configured_fields() if self._config else []):
            lowered = str(field).lower()
            if lowered not in available:
                self._field_available_map[field] = False
                self._warnings.append(f"Field '{field}' not found on Opportunity; skipping.")
                continue
            if lowered not in self._available_fields:
                self._field_available_map[field] = False
                self._permission_blocked_fields.append(field)
                continue
            self._field_available_map[field] = True

        if self._permission_blocked_fields:
            blocked = ", ".join(sorted(self._permission_blocked_fields))
            self._warnings.append(f"field_unavailable_due_to_permissions: {blocked}")

        return PreflightResult(ok=True)

    def _field_available(self, field_name: str | None) -> bool:
        if not field_name:
            return False
        return self._field_available_map.get(field_name, True)

    def build_count_filters(self) -> Tuple[str, List[FilterExpr]]:
        cfg = self._config or load_dealdesk_config(self.config_path)
        since_days = int(self.params.get("since_days") or 7)
        start_date = (_utc_today() - timedelta(days=since_days)).isoformat()
        filters: List[FilterExpr] = [
            Filter("StageName", FilterOp.EQ, cfg.stages.close_won_stage),
            Filter("CloseDate", FilterOp.GTE, start_date),
        ]
        return "Opportunity", filters

    def build_sample_request(self) -> Tuple[str, List[str], List[FilterExpr], str | None, str, bool]:
        cfg = self._config or load_dealdesk_config(self.config_path)
        fields = {
            "Id",
            "Name",
            "StageName",
            "Amount",
            "CloseDate",
            "OwnerId",
            "AccountId",
        }
        for field in cfg.configured_fields():
            if self._field_available(field):
                fields.add(field)
        filters = self.build_count_filters()[1]
        return "Opportunity", sorted(fields), filters, "CloseDate", "DESC", False

    def evaluate(self, rows: List[Dict[str, Any]]) -> List[EvidenceRow]:
        cfg = self._config or load_dealdesk_config(self.config_path)
        today = _utc_today()
        evidence: List[EvidenceRow] = []

        amount_rules = cfg.amount_rules
        amount_field = amount_rules.amount_field
        term_field = amount_rules.term_months_field
        tcv_field = amount_rules.tcv_field
        arr_field = amount_rules.arr_field
        acv_field = amount_rules.acv_field
        tolerance_pct = amount_rules.amount_vs_line_items_tolerance_pct

        opp_ids = [row.get("Id") for row in rows if row.get("Id")]
        line_item_totals = self._fetch_line_item_totals(opp_ids) if amount_rules.line_item_sobject else {}
        attachment_counts = (
            self._fetch_attachment_counts(opp_ids)
            if cfg.attachment_rules.enabled and cfg.attachment_rules.sources
            else {}
        )

        for row in rows:
            opp_id = row.get("Id", "")
            stage = row.get("StageName") or ""
            amount = _to_float(row.get(amount_field))
            band = cfg.resolve_size_band(amount)

            check_results: list[dict[str, Any]] = []
            proposed_diffs: dict[str, Any] = {}
            computed_suggestions: dict[str, Any] = {}
            reason_codes: list[str] = []

            missing_fields: list[str] = []
            required = cfg.required_fields_for(stage, band)
            for field in required:
                if not self._field_available(field):
                    continue
                if not row.get(field):
                    missing_fields.append(field)
                    code = f"DD_MISSING_FIELD:{field}"
                    reason_codes.append(code)
                    check_results.append({
                        "code": code,
                        "status": "fail",
                        "detail": "Field is blank",
                        "severity": "high",
                    })
            if not missing_fields:
                check_results.append({
                    "code": "DD_REQUIRED_FIELDS_OK",
                    "status": "pass",
                    "detail": "All required fields present",
                    "severity": "low",
                })

            close_date = _parse_date(row.get("CloseDate"))
            if close_date:
                future_limit = today + timedelta(days=cfg.date_rules.close_date_not_future_days)
                stale_limit = today - timedelta(days=cfg.date_rules.close_date_not_stale_days)
                if close_date > future_limit:
                    reason_codes.append("DD_CLOSE_DATE_FUTURE")
                    check_results.append({
                        "code": "DD_CLOSE_DATE_FUTURE",
                        "status": "fail",
                        "detail": f"CloseDate {close_date} > {future_limit}",
                        "severity": "high",
                    })
                else:
                    check_results.append({
                        "code": "DD_CLOSE_DATE_FUTURE",
                        "status": "pass",
                        "detail": "Within future window",
                        "severity": "low",
                    })

                if close_date < stale_limit:
                    reason_codes.append("DD_CLOSE_DATE_STALE")
                    check_results.append({
                        "code": "DD_CLOSE_DATE_STALE",
                        "status": "fail",
                        "detail": f"CloseDate {close_date} < {stale_limit}",
                        "severity": "medium",
                    })
                else:
                    check_results.append({
                        "code": "DD_CLOSE_DATE_STALE",
                        "status": "pass",
                        "detail": "Within stale window",
                        "severity": "low",
                    })

            if amount_rules.amount_must_be_positive:
                if amount is not None and amount <= 0:
                    reason_codes.append("DD_AMOUNT_NEGATIVE")
                    check_results.append({
                        "code": "DD_AMOUNT_NEGATIVE",
                        "status": "fail",
                        "detail": f"Amount {amount} <= 0",
                        "severity": "high",
                    })
                else:
                    check_results.append({
                        "code": "DD_AMOUNT_NEGATIVE",
                        "status": "pass",
                        "detail": "Amount positive",
                        "severity": "low",
                    })

            if amount is not None and opp_id in line_item_totals:
                total = line_item_totals.get(opp_id, 0.0)
                tol = abs(amount) * (tolerance_pct / 100.0)
                if abs(amount - total) > tol:
                    reason_codes.append("DD_AMOUNT_MISMATCH_LINE_ITEMS")
                    check_results.append({
                        "code": "DD_AMOUNT_MISMATCH_LINE_ITEMS",
                        "status": "fail",
                        "detail": f"Amount {amount} vs line items {total}",
                        "severity": "medium",
                    })
                else:
                    check_results.append({
                        "code": "DD_AMOUNT_MISMATCH_LINE_ITEMS",
                        "status": "pass",
                        "detail": "Amount aligns to line items",
                        "severity": "low",
                    })

            term_months, term_formula = self._resolve_term_months(row.get(term_field), amount_rules.term_unit)

            if tcv_field and self._field_available(tcv_field):
                tcv_value = _to_float(row.get(tcv_field))
                expected_tcv = self._expected_tcv(amount, term_months, amount_rules.amount_semantics)
                if tcv_value is not None and expected_tcv is not None:
                    diff = abs(tcv_value - expected_tcv)
                    tol = abs(expected_tcv) * (tolerance_pct / 100.0)
                    if diff > tol:
                        reason_codes.append("DD_AMOUNT_MISMATCH_TCV")
                        check_results.append({
                            "code": "DD_AMOUNT_MISMATCH_TCV",
                            "status": "fail",
                            "detail": f"TCV {tcv_value} != expected {expected_tcv}",
                            "severity": "medium",
                        })
                    else:
                        check_results.append({
                            "code": "DD_AMOUNT_MISMATCH_TCV",
                            "status": "pass",
                            "detail": "TCV aligns with Amount",
                            "severity": "low",
                        })

            if arr_field and self._field_available(arr_field):
                expected_arr = self._expected_arr(amount, term_months, amount_rules.amount_semantics)
                expected_arr = self._apply_rounding(expected_arr, amount_rules.rounding)
                arr_value = _to_float(row.get(arr_field))
                if expected_arr is not None:
                    diff = abs((arr_value or 0.0) - expected_arr) if arr_value is not None else None
                    tol = abs(expected_arr) * (tolerance_pct / 100.0)
                    if arr_value is None or (diff is not None and diff > tol):
                        reason_codes.append("DD_ARR_INCONSISTENT")
                        detail = (
                            "ARR missing" if arr_value is None else f"ARR {arr_value} != expected {expected_arr}"
                        )
                        check_results.append({
                            "code": "DD_ARR_INCONSISTENT",
                            "status": "fail",
                            "detail": detail,
                            "severity": "medium",
                        })
                        if amount is not None and term_months:
                            formula = f"Amount / Term * 12 = {amount} / {term_months} * 12"
                            if term_formula:
                                formula = (
                                    f"Amount / ({term_formula}) * 12 "
                                    f"= {amount} / {term_months} * 12"
                                )
                            proposed_diffs[arr_field] = {
                                "before": arr_value,
                                "after": expected_arr,
                                "formula": formula,
                                "inputs": {"Amount": amount, "Term": term_months},
                            }
                            computed_suggestions[arr_field] = {
                                "value": expected_arr,
                                "formula": formula,
                                "inputs": {"Amount": amount, "Term": term_months},
                            }
                    else:
                        check_results.append({
                            "code": "DD_ARR_INCONSISTENT",
                            "status": "pass",
                            "detail": "ARR aligns with Amount/Term",
                            "severity": "low",
                        })

            if acv_field and self._field_available(acv_field):
                expected_acv = self._expected_arr(amount, term_months, amount_rules.amount_semantics)
                expected_acv = self._apply_rounding(expected_acv, amount_rules.rounding)
                acv_value = _to_float(row.get(acv_field))
                if expected_acv is not None and acv_value is not None:
                    diff = abs(acv_value - expected_acv)
                    tol = abs(expected_acv) * (tolerance_pct / 100.0)
                    if diff > tol:
                        reason_codes.append("DD_ACV_INCONSISTENT")
                        check_results.append({
                            "code": "DD_ACV_INCONSISTENT",
                            "status": "fail",
                            "detail": f"ACV {acv_value} != expected {expected_acv}",
                            "severity": "medium",
                        })

            start_field = cfg.date_rules.contract_start_field
            end_field = cfg.date_rules.contract_end_field
            if start_field and end_field and self._field_available(start_field) and self._field_available(end_field):
                start_date = _parse_date(row.get(start_field))
                end_date = _parse_date(row.get(end_field))
                invalid = False
                if start_date and close_date and cfg.date_rules.contract_start_after_close:
                    if start_date < close_date:
                        invalid = True
                if end_date and start_date and cfg.date_rules.contract_end_after_start:
                    if end_date < start_date:
                        invalid = True
                if invalid:
                    reason_codes.append("DD_CONTRACT_DATES_INVALID")
                    check_results.append({
                        "code": "DD_CONTRACT_DATES_INVALID",
                        "status": "fail",
                        "detail": "Contract dates out of order",
                        "severity": "medium",
                    })
                else:
                    check_results.append({
                        "code": "DD_CONTRACT_DATES_INVALID",
                        "status": "pass",
                        "detail": "Contract dates valid",
                        "severity": "low",
                    })

            if cfg.attachment_rules.enabled:
                count = attachment_counts.get(opp_id, 0)
                if count < cfg.attachment_rules.min_attachments:
                    reason_codes.append("DD_NO_ATTACHMENT")
                    severity = "high" if cfg.attachment_rules.block_close_if_missing else "medium"
                    check_results.append({
                        "code": "DD_NO_ATTACHMENT",
                        "status": "fail",
                        "detail": f"Attachments {count} < {cfg.attachment_rules.min_attachments}",
                        "severity": severity,
                    })
                else:
                    check_results.append({
                        "code": "DD_NO_ATTACHMENT",
                        "status": "pass",
                        "detail": "Attachments present",
                        "severity": "low",
                    })

            overall_status = "fail" if any(item.get("status") == "fail" for item in check_results) else "pass"
            renewal_eligible = overall_status == "pass" and cfg.renewal.enabled
            renewal_preview = self._build_renewal_preview(row, cfg) if renewal_eligible else None

            fields = dict(row)
            fields.update(
                {
                    "check_results": check_results,
                    "band": band,
                    "missing_fields": missing_fields,
                    "proposed_diffs": proposed_diffs,
                    "computed_suggestions": computed_suggestions,
                    "reason_codes": reason_codes,
                    "overall_status": overall_status,
                    "renewal_eligible": renewal_eligible,
                    "renewal_proposed": renewal_preview,
                    "field_unavailable_due_to_permissions": list(self._permission_blocked_fields),
                }
            )

            reason = "Deal Desk verification passed" if overall_status == "pass" else "Deal Desk verification failed"
            if reason_codes:
                reason = f"{reason}: {', '.join(reason_codes)}"
            reason_code = reason_codes[0] if reason_codes else "DEALDESK_CLOSE_VERIFICATION"
            severity = "high" if overall_status == "fail" else "low"

            evidence.append(
                EvidenceRow(
                    record_id=opp_id,
                    sobject="Opportunity",
                    fields=fields,
                    reason=reason,
                    reason_code=reason_code,
                    severity=severity,
                )
            )

        return evidence

    def _build_renewal_preview(self, row: Dict[str, Any], cfg: DealDeskConfig) -> Dict[str, Any]:
        close_date = _parse_date(row.get("CloseDate")) or _utc_today()
        renewal_close = close_date + timedelta(days=cfg.renewal.date_offset_days)
        name = cfg.renewal.name_template.format(
            original_name=row.get("Name") or "Opportunity",
            year=renewal_close.year,
        )
        preview: Dict[str, Any] = {
            "Name": name,
            "CloseDate": renewal_close.isoformat(),
            "StageName": cfg.renewal.stage_default,
        }
        for field in cfg.renewal.copy_fields:
            if row.get(field) is not None:
                preview[field] = row.get(field)
        if cfg.renewal.provenance_field and self._field_available(cfg.renewal.provenance_field):
            preview[cfg.renewal.provenance_field] = row.get("Id")
        return preview

    def _resolve_term_months(self, term_value: Any, unit: str) -> Tuple[float | None, str | None]:
        term = _to_float(term_value)
        if term is None:
            return None, None
        unit = (unit or "months").lower()
        if unit == "months":
            return term, f"Term {term} months"
        if unit == "years":
            return term * 12, f"Term {term} years * 12"
        if unit == "days":
            months = term / 30.0
            return months, f"Term {term} days / 30"
        return term, f"Term {term} months"

    def _expected_tcv(self, amount: float | None, term_months: float | None, semantics: str) -> float | None:
        if amount is None:
            return None
        semantics = (semantics or "tcv").lower()
        if semantics == "acv" and term_months:
            return amount * (term_months / 12.0)
        return amount

    def _expected_arr(self, amount: float | None, term_months: float | None, semantics: str) -> float | None:
        if amount is None:
            return None
        semantics = (semantics or "tcv").lower()
        if semantics == "tcv":
            if not term_months:
                return None
            return amount / term_months * 12.0
        return amount

    def _apply_rounding(self, value: float | None, rounding: str) -> float | None:
        if value is None:
            return None
        rounding = (rounding or "cents").lower()
        if rounding == "dollars":
            return float(round(value))
        return float(round(value, 2))

    def _fetch_line_item_totals(self, opp_ids: List[str]) -> Dict[str, float]:
        if not opp_ids:
            return {}
        cfg = self._config or load_dealdesk_config(self.config_path)
        sobject = cfg.amount_rules.line_item_sobject
        if not sobject or not self.provider.supports_object(sobject):
            return {}
        filters = [Filter("OpportunityId", FilterOp.IN, opp_ids)]
        try:
            rows = self.provider.query(sobject, ["OpportunityId", "TotalPrice"], filters, limit=2000)
        except Exception:
            self._warnings.append("Unable to query line items; skipping line item checks.")
            return {}
        totals: Dict[str, float] = {}
        for row in rows:
            opp_id = row.get("OpportunityId")
            total_price = _to_float(row.get("TotalPrice")) or 0.0
            if opp_id:
                totals[opp_id] = totals.get(opp_id, 0.0) + total_price
        return totals

    def _fetch_attachment_counts(self, opp_ids: List[str]) -> Dict[str, int]:
        counts: Dict[str, int] = {}
        if not opp_ids:
            return counts
        cfg = self._config or load_dealdesk_config(self.config_path)
        for source in cfg.attachment_rules.sources:
            if source == "ContentDocumentLink":
                if not self.provider.supports_object("ContentDocumentLink"):
                    continue
                filters = [Filter("LinkedEntityId", FilterOp.IN, opp_ids)]
                try:
                    rows = self.provider.query(
                        "ContentDocumentLink",
                        ["LinkedEntityId"],
                        filters,
                        limit=2000,
                    )
                except Exception:
                    self._warnings.append("Unable to query ContentDocumentLink; skipping attachment checks.")
                    continue
                for row in rows:
                    key = row.get("LinkedEntityId")
                    if key:
                        counts[key] = counts.get(key, 0) + 1
            elif source == "Attachment":
                if not self.provider.supports_object("Attachment"):
                    continue
                filters = [Filter("ParentId", FilterOp.IN, opp_ids)]
                try:
                    rows = self.provider.query(
                        "Attachment",
                        ["ParentId"],
                        filters,
                        limit=2000,
                    )
                except Exception:
                    self._warnings.append("Unable to query Attachment; skipping attachment checks.")
                    continue
                for row in rows:
                    key = row.get("ParentId")
                    if key:
                        counts[key] = counts.get(key, 0) + 1
        return counts

    def _findings_count(self, count: int, evidence: List[EvidenceRow]) -> int:
        return sum(1 for row in evidence if row.fields.get("overall_status") == "fail")

    def _derive_status(self, count: int, evidence: List[EvidenceRow]) -> str:
        fails = self._findings_count(count, evidence)
        return "findings" if fails > 0 else "ok"

    def get_recommended_actions(self) -> List[RecommendedAction]:
        return [
            RecommendedAction(
                action_type="sfdc.record.update",
                label="Fix missing/incorrect fields",
                risk_level=1,
                details={"sobject": "Opportunity", "policy_tags": ["dealdesk", "reversible_write"]},
            ),
            RecommendedAction(
                action_type="dealdesk.renewal.create_shell",
                label="Create renewal opportunity shell",
                risk_level=2,
                details={"policy_tags": ["dealdesk"]},
            ),
        ]


__all__ = ["DealDeskCloseVerificationCheck"]
