"""Do-Not-Contact compliance scout check."""

from __future__ import annotations

import time
from typing import TYPE_CHECKING, Any, Dict, List, Tuple

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

if TYPE_CHECKING:
    from g_gremlin.outreach import OutreachClient


class DoNotContactCheck(BaseScoutCheck):
    _OUTREACH_ACTIVE_STATES = {"active", "in_progress", "pending", "scheduled", "running"}

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self._current_object = "Lead"
        self._suppression_fields_by_object: dict[str, list[str]] = {}
        self._open_case_contact_ids: set[str] = set()
        self._sequence_lookup_cache: dict[str, dict[str, Any]] = {}
        self._outreach_client: OutreachClient | None = None
        self._outreach_warning_emitted = False
        self._action_record_ids_by_reason: dict[str, set[str]] = {}

    def preflight(self) -> PreflightResult:
        base = super().preflight()
        if base.unsupported:
            return base

        target_objects = ["Lead"]
        if bool(self.params.get("include_contacts", True)):
            target_objects.append("Contact")
        self._prepare_suppression_fields(target_objects)

        if bool(self.params.get("include_open_tickets", True)) and "Contact" in target_objects:
            if not self.provider.supports_object("Case"):
                self._warnings.append("Case object unsupported; open-ticket sub-check skipped.")

        return PreflightResult(ok=True)

    def build_count_filters(self) -> Tuple[str, List[FilterExpr]]:
        object_type = self._current_object
        return object_type, self._base_filters_for_object(object_type)

    def build_sample_request(self) -> Tuple[str, List[str], List[FilterExpr], str | None, str, bool]:
        object_type = self._current_object
        available = self._available_fields(object_type)

        fields: list[str] = ["Id", "Name", "Email", "OwnerId"]
        for preferred in ("HasOptedOutOfEmail", "DoNotCall"):
            if preferred.lower() in available and preferred not in fields:
                fields.append(preferred)
        for suppression_field in self._suppression_fields_by_object.get(object_type.lower(), []):
            if suppression_field not in fields:
                fields.append(suppression_field)

        order_field = None
        if "lastmodifieddate" in available:
            order_field = "LastModifiedDate"
            if order_field not in fields:
                fields.append(order_field)
        elif "createddate" in available:
            order_field = "CreatedDate"
            if order_field not in fields:
                fields.append(order_field)

        return object_type, fields, self._base_filters_for_object(object_type), order_field, "DESC", False

    def evaluate(self, rows: List[Dict[str, Any]]) -> List[EvidenceRow]:
        evidence: List[EvidenceRow] = []
        for row in rows:
            record_id = str(row.get("Id") or "").strip()
            if not record_id:
                continue

            sobject = str(row.get("_sobject") or self._current_object or "Lead")
            suppression_matches = self._matched_suppression_fields(sobject, row)
            if not suppression_matches:
                continue

            email = str(row.get("Email") or "").strip()
            sequence_context = self._lookup_sequence_context(email)
            active_state_ids = list(sequence_context.get("active_sequence_state_ids", []))
            active_sequence_ids = list(sequence_context.get("active_sequence_ids", []))
            prospect_id = sequence_context.get("prospect_id")

            has_open_ticket = sobject.lower() == "contact" and record_id in self._open_case_contact_ids
            if has_open_ticket and active_state_ids:
                reason_code = "SUPPORT_ESCALATION_IN_SEQUENCE"
                severity = "high"
            else:
                reason_code = "DNC_IN_ACTIVE_SEQUENCE"
                severity = "critical"

            if has_open_ticket and active_state_ids:
                reason = "Contact has open support case and active sequence enrollment."
            elif has_open_ticket:
                reason = "Contact has open support case and is DNC-flagged."
            elif active_state_ids:
                reason = "DNC-flagged record is enrolled in an active sequence."
            elif not bool(sequence_context.get("outreach_connected")):
                reason = "DNC-flagged record (Outreach unavailable; sequence status unverified)."
            else:
                reason = "DNC-flagged record."

            fields_payload: dict[str, Any] = {
                "Name": row.get("Name"),
                "Email": email,
                "OwnerId": row.get("OwnerId"),
                "suppression_fields_matched": suppression_matches,
                "outreach_connected": bool(sequence_context.get("outreach_connected")),
                "active_sequence_count": len(active_state_ids),
                "active_sequence_ids": active_sequence_ids,
                "sequence_state_ids": active_state_ids,
                "open_support_ticket": has_open_ticket,
            }
            if prospect_id:
                fields_payload["outreach_prospect_id"] = prospect_id

            related_ids = list(active_state_ids)
            if prospect_id:
                related_ids.append(str(prospect_id))

            evidence.append(
                EvidenceRow(
                    record_id=record_id,
                    sobject=sobject,
                    fields=fields_payload,
                    reason=reason,
                    reason_code=reason_code,
                    severity=severity,
                    related_ids=related_ids,
                )
            )
        return evidence

    def execute(self) -> ScoutFinding:  # type: ignore[override]
        start = time.perf_counter()
        try:
            preflight = self.preflight()
            if preflight.unsupported:
                return self._unsupported_finding(preflight.reason or "Unsupported")

            include_contacts = bool(self.params.get("include_contacts", True))
            include_open_tickets = bool(self.params.get("include_open_tickets", True))
            target_objects = ["Lead"] + (["Contact"] if include_contacts else [])

            self._outreach_client = self._build_outreach_client()
            self._open_case_contact_ids = (
                self._load_open_case_contact_ids() if include_open_tickets and include_contacts else set()
            )

            total_scanned = 0
            full_evidence: list[EvidenceRow] = []

            for object_type in target_objects:
                if not self.provider.supports_object(object_type):
                    self._warnings.append(f"Unsupported object: {object_type}")
                    continue

                self._current_object = object_type
                _, count_filters = self.build_count_filters()
                count = self.provider.count(object_type, count_filters)
                self._collect_provider_meta()
                total_scanned += count
                if count <= 0:
                    continue

                obj_type, fields, filters, order_by, order_dir, nulls_first = self.build_sample_request()
                rows = self.provider.query(
                    obj_type,
                    fields,
                    filters,
                    order_by=order_by,
                    order_dir=order_dir,
                    nulls_first=nulls_first,
                    limit=self.top_n,
                )
                self._collect_provider_meta()
                typed_rows = []
                for row in rows:
                    row_copy = dict(row)
                    row_copy["_sobject"] = object_type
                    typed_rows.append(row_copy)
                full_evidence.extend(self.evaluate(typed_rows))

            findings_count = len(full_evidence)
            for row in full_evidence:
                if row.record_id:
                    bucket = self._action_record_ids_by_reason.setdefault(
                        row.reason_code or "DNC_IN_ACTIVE_SEQUENCE",
                        set(),
                    )
                    bucket.add(row.record_id)

            evidence = full_evidence[: self.top_n]
            status = "findings" if findings_count > 0 else "ok"
            duration_ms = int((time.perf_counter() - start) * 1000)
            return ScoutFinding(
                scout_name=self.definition.name,
                display_name=self.definition.display_name,
                description=self.definition.description,
                category=self.definition.category,
                severity=self.definition.severity_default,
                total_records_scanned=total_scanned,
                findings_count=findings_count,
                evidence=evidence,
                recommended_actions=self.get_recommended_actions(),
                soql_queries=self._soql_queries,
                duration_ms=duration_ms,
                parameters_used=self.params,
                status=status,
                warnings=self._warnings,
            )
        except Exception as exc:
            duration_ms = int((time.perf_counter() - start) * 1000)
            return self._error_finding(str(exc), duration_ms=duration_ms)

    def get_recommended_actions(self) -> List[RecommendedAction]:
        actions: list[RecommendedAction] = []
        for reason_code in sorted(self._action_record_ids_by_reason.keys()):
            record_ids = sorted(self._action_record_ids_by_reason.get(reason_code, set()))
            if not record_ids:
                continue
            actions.append(
                RecommendedAction(
                    action_type="pause_sequence",
                    label="Pause sequence enrollment for DNC-flagged records",
                    risk_level="low",
                    details={"reason_code": reason_code, "record_ids": record_ids},
                )
            )
        return actions

    def _build_outreach_client(self) -> OutreachClient | None:
        try:
            from g_gremlin.outreach import OutreachClient

            return OutreachClient()
        except Exception:
            if not self._outreach_warning_emitted:
                self._warnings.append(
                    "Outreach not configured; DNC findings will not include live sequence state checks."
                )
                self._outreach_warning_emitted = True
            return None

    def _load_open_case_contact_ids(self) -> set[str]:
        if not self.provider.supports_object("Case"):
            return set()

        case_fields = self._available_fields("Case")
        if "contactid" not in case_fields:
            self._warnings.append("Case.ContactId is unavailable; open-ticket sub-check skipped.")
            return set()

        filters: list[FilterExpr] = [Filter("ContactId", FilterOp.IS_NOT_NULL)]
        if "status" in case_fields:
            filters.append(Filter("Status", FilterOp.NEQ, "Closed"))

        lookback_days = int(self.params.get("lookback_days", 90) or 90)
        if lookback_days > 0:
            if "lastmodifieddate" in case_fields:
                filters.append(Filter("LastModifiedDate", FilterOp.LAST_N_DAYS, lookback_days))
            elif "createddate" in case_fields:
                filters.append(Filter("CreatedDate", FilterOp.LAST_N_DAYS, lookback_days))

        order_by = "LastModifiedDate" if "lastmodifieddate" in case_fields else None
        query_fields = ["ContactId"]
        if "status" in case_fields:
            query_fields.append("Status")
        rows = self.provider.query(
            "Case",
            query_fields,
            filters,
            order_by=order_by,
            order_dir="DESC",
            limit=max(self.top_n * 10, 200),
        )
        self._collect_provider_meta()

        contact_ids: set[str] = set()
        for row in rows:
            contact_id = str(row.get("ContactId") or "").strip()
            if contact_id:
                contact_ids.add(contact_id)
        return contact_ids

    def _prepare_suppression_fields(self, object_types: list[str]) -> None:
        configured_fields = self._configured_suppression_fields()
        for object_type in object_types:
            if not self.provider.supports_object(object_type):
                continue
            available = self._available_fields(object_type)
            usable: list[str] = []
            for field in configured_fields:
                if field.lower() in available:
                    usable.append(field)
                else:
                    self._warnings.append(f"{object_type}: suppression field '{field}' not found; skipping.")
            self._suppression_fields_by_object[object_type.lower()] = usable

    def _configured_suppression_fields(self) -> list[str]:
        base_fields = self.params.get("suppression_fields") or ["HasOptedOutOfEmail", "DoNotCall"]
        ordered: list[str] = []
        for value in base_fields:
            field_name = str(value or "").strip()
            if field_name and field_name not in ordered:
                ordered.append(field_name)
        custom_field = str(self.params.get("custom_suppression_field") or "").strip()
        if custom_field and custom_field not in ordered:
            ordered.append(custom_field)
        return ordered

    def _base_filters_for_object(self, object_type: str) -> list[FilterExpr]:
        filters: list[FilterExpr] = []
        suppression_fields = self._suppression_fields_by_object.get(object_type.lower(), [])
        if suppression_fields:
            filters.append(OrGroup([Filter(field, FilterOp.EQ, True) for field in suppression_fields]))
        else:
            # No compatible suppression fields means this check cannot produce signal.
            # Use an impossible filter rather than scanning full objects.
            filters.append(Filter("Id", FilterOp.IS_NULL))

        available = self._available_fields(object_type)
        lookback_days = int(self.params.get("lookback_days", 90) or 90)
        if lookback_days > 0:
            if "lastmodifieddate" in available:
                filters.append(Filter("LastModifiedDate", FilterOp.LAST_N_DAYS, lookback_days))
            elif "createddate" in available:
                filters.append(Filter("CreatedDate", FilterOp.LAST_N_DAYS, lookback_days))
        return filters

    def _available_fields(self, object_type: str) -> set[str]:
        available_method = getattr(self.provider, "available_fields", None)
        if callable(available_method):
            try:
                return {str(name).lower() for name in available_method(object_type)}
            except Exception:
                pass

        try:
            describe = self.provider.describe(object_type)
        except Exception:
            return set()
        fields = describe.get("result", {}).get("fields", []) if isinstance(describe, dict) else []
        if not isinstance(fields, list):
            return set()
        return {
            str(item.get("name")).lower()
            for item in fields
            if isinstance(item, dict) and item.get("name")
        }

    def _matched_suppression_fields(self, object_type: str, row: Dict[str, Any]) -> list[str]:
        matched: list[str] = []
        for field in self._suppression_fields_by_object.get(object_type.lower(), []):
            if self._is_truthy(row.get(field)):
                matched.append(field)
        return matched

    def _lookup_sequence_context(self, email: str) -> dict[str, Any]:
        blank = {
            "outreach_connected": bool(self._outreach_client),
            "prospect_id": None,
            "active_sequence_state_ids": [],
            "active_sequence_ids": [],
        }
        normalized = str(email or "").strip().lower()
        if not normalized:
            return dict(blank)
        if normalized in self._sequence_lookup_cache:
            return dict(self._sequence_lookup_cache[normalized])
        if self._outreach_client is None:
            self._sequence_lookup_cache[normalized] = dict(blank)
            return dict(blank)

        try:
            prospect = self._outreach_client.find_prospect_by_email(normalized)
            if prospect is None or not getattr(prospect, "id", None):
                resolved = dict(blank)
                resolved["outreach_connected"] = True
                self._sequence_lookup_cache[normalized] = resolved
                return dict(resolved)

            prospect_id = str(prospect.id)
            states = self._outreach_client.get_sequence_states_for_prospect(prospect_id)
            state_ids: list[str] = []
            sequence_ids: list[str] = []
            for state in states:
                if not isinstance(state, dict):
                    continue
                attributes = state.get("attributes") if isinstance(state.get("attributes"), dict) else {}
                state_value = str(attributes.get("state") or "").strip().lower()
                if state_value not in self._OUTREACH_ACTIVE_STATES:
                    continue
                state_id = state.get("id")
                if state_id:
                    state_ids.append(str(state_id))
                seq_id = self._sequence_id_from_state(state)
                if seq_id and seq_id not in sequence_ids:
                    sequence_ids.append(seq_id)

            resolved = {
                "outreach_connected": True,
                "prospect_id": prospect_id,
                "active_sequence_state_ids": state_ids,
                "active_sequence_ids": sequence_ids,
            }
            self._sequence_lookup_cache[normalized] = resolved
            return dict(resolved)
        except Exception as exc:
            self._warnings.append(f"Outreach sequence lookup failed for {normalized}: {exc}")
            unresolved = dict(blank)
            unresolved["outreach_connected"] = False
            self._sequence_lookup_cache[normalized] = unresolved
            return dict(unresolved)

    def _sequence_id_from_state(self, state: dict[str, Any]) -> str | None:
        relationships = state.get("relationships") if isinstance(state.get("relationships"), dict) else {}
        sequence_rel = relationships.get("sequence") if isinstance(relationships, dict) else {}
        sequence_data = sequence_rel.get("data") if isinstance(sequence_rel, dict) else {}
        sequence_id = sequence_data.get("id") if isinstance(sequence_data, dict) else None
        if sequence_id:
            return str(sequence_id)
        attributes = state.get("attributes") if isinstance(state.get("attributes"), dict) else {}
        if attributes.get("sequenceId"):
            return str(attributes.get("sequenceId"))
        return None

    def _is_truthy(self, value: Any) -> bool:
        if isinstance(value, bool):
            return value
        if isinstance(value, (int, float)):
            return value != 0
        if isinstance(value, str):
            lowered = value.strip().lower()
            return lowered in {"true", "1", "yes", "y"}
        return bool(value)


__all__ = ["DoNotContactCheck"]
