"""Existing customer and open-ticket outbound guardrail check."""

from __future__ import annotations

import time
from datetime import datetime, timedelta, timezone
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
from ..utils import chunked

if TYPE_CHECKING:
    from g_gremlin.outreach import OutreachClient


class CustomerOpenTicketGuardrailCheck(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._outreach_client: OutreachClient | None = None
        self._outreach_warning_emitted = False
        self._sequence_lookup_cache: dict[str, dict[str, Any]] = {}
        self._active_opportunity_accounts: set[str] = set()
        self._open_case_accounts: set[str] = set()
        self._open_case_contacts: set[str] = set()
        self._recent_churn_accounts: set[str] = set()
        self._pause_record_ids_by_reason: dict[str, set[str]] = {}
        self._route_record_ids_by_reason: dict[str, set[str]] = {}

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

        target_objects = self._target_objects()
        for object_type in target_objects:
            if not self.provider.supports_object(object_type):
                self._warnings.append(f"Unsupported object: {object_type}")
                continue
            available = self._available_fields(object_type)
            if "email" not in available:
                self._warnings.append(f"{object_type}: Email field unavailable; outbound guardrail skipped for this object.")
        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, self._available_fields(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, order_by = self._fields_for_object(object_type, available)
        filters = self._base_filters_for_object(object_type, available)
        return object_type, fields, filters, order_by, "DESC", False

    def evaluate(self, rows: List[Dict[str, Any]]) -> List[EvidenceRow]:
        evidence: list[EvidenceRow] = []
        for row in rows:
            candidate = self._evaluate_row(row)
            if candidate is not None:
                evidence.append(candidate)
        return evidence

    def execute(self) -> ScoutFinding:  # type: ignore[override]
        start = time.perf_counter()
        self._sequence_lookup_cache = {}
        self._pause_record_ids_by_reason = {}
        self._route_record_ids_by_reason = {}
        self._active_opportunity_accounts = set()
        self._open_case_accounts = set()
        self._open_case_contacts = set()
        self._recent_churn_accounts = set()

        try:
            preflight = self.preflight()
            if preflight.unsupported:
                return self._unsupported_finding(preflight.reason or "Unsupported")

            self._outreach_client = self._build_outreach_client()
            query_limit = self._query_limit()
            target_objects = self._target_objects()

            total_scanned = 0
            candidate_rows: list[dict[str, Any]] = []
            account_ids: set[str] = set()
            contact_ids: set[str] = set()

            for object_type in target_objects:
                if not self.provider.supports_object(object_type):
                    continue
                available = self._available_fields(object_type)
                if "email" not in available:
                    continue

                self._current_object = object_type
                filters = self._base_filters_for_object(object_type, available)
                count = self.provider.count(object_type, filters)
                self._collect_provider_meta()
                total_scanned += count
                if count <= 0:
                    continue

                if count > query_limit:
                    self._warnings.append(
                        f"{object_type}: scan capped at {query_limit} rows out of {count}; findings may be partial."
                    )

                fields, order_by = self._fields_for_object(object_type, available)
                rows = self.provider.query(
                    object_type,
                    fields,
                    filters,
                    order_by=order_by,
                    order_dir="DESC",
                    nulls_first=False,
                    limit=query_limit,
                )
                self._collect_provider_meta()
                typed_rows = [self._attach_sobject(row, object_type) for row in rows]
                candidate_rows.extend(typed_rows)
                account_ids.update(self._extract_account_ids(typed_rows))
                if object_type.lower() == "contact":
                    contact_ids.update(self._extract_contact_ids(typed_rows))

            self._active_opportunity_accounts = self._load_active_opportunity_accounts(account_ids)
            self._open_case_accounts, self._open_case_contacts = self._load_open_case_signals(account_ids, contact_ids)
            self._recent_churn_accounts = self._load_recent_churn_accounts(account_ids)

            evidence_all: list[EvidenceRow] = []
            if self._outreach_client is None:
                status = "ok"
            else:
                evidence_all = self.evaluate(candidate_rows)
                status = "findings" if evidence_all else "ok"

            findings_count = len(evidence_all)
            evidence = evidence_all[: self.top_n]
            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._pause_record_ids_by_reason.keys()):
            record_ids = sorted(self._pause_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 guarded customer records",
                    risk_level="low",
                    details={"reason_code": reason_code, "record_ids": record_ids},
                )
            )
        for reason_code in sorted(self._route_record_ids_by_reason.keys()):
            record_ids = sorted(self._route_record_ids_by_reason.get(reason_code, set()))
            if not record_ids:
                continue
            actions.append(
                RecommendedAction(
                    action_type="route_exception",
                    label="Route outbound guardrail exceptions for owner review",
                    risk_level="low",
                    details={"reason_code": reason_code, "record_ids": record_ids},
                )
            )
        return actions

    def _evaluate_row(self, row: Dict[str, Any]) -> EvidenceRow | None:
        record_id = str(row.get("Id") or "").strip()
        if not record_id:
            return None

        email = str(row.get("Email") or "").strip().lower()
        if not email:
            return None

        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", []))
        if not active_state_ids:
            return None

        account_id = str(row.get("AccountId") or "").strip()
        contact_id = str(row.get("Id") or "").strip() if str(row.get("_sobject") or "").lower() == "contact" else ""
        has_active_opp = bool(account_id and account_id in self._active_opportunity_accounts)
        has_open_case = bool(
            (account_id and account_id in self._open_case_accounts)
            or (contact_id and contact_id in self._open_case_contacts)
        )
        has_recent_churn = bool(account_id and account_id in self._recent_churn_accounts)
        if not (has_active_opp or has_open_case or has_recent_churn):
            return None

        reason_code, reason = self._reason_for_signals(
            has_active_opp=has_active_opp,
            has_open_case=has_open_case,
            has_recent_churn=has_recent_churn,
        )
        sobject = str(row.get("_sobject") or self._current_object or "Lead")
        fields_payload: dict[str, Any] = {
            "Name": row.get("Name"),
            "Email": email,
            "OwnerId": row.get("OwnerId"),
            "AccountId": account_id,
            "active_sequence_count": len(active_state_ids),
            "active_sequence_ids": active_sequence_ids,
            "active_sequence_state_ids": active_state_ids,
            "has_active_opportunity": has_active_opp,
            "has_open_support_ticket": has_open_case,
            "has_recent_churn": has_recent_churn,
        }
        if row.get("Company"):
            fields_payload["Company"] = row.get("Company")

        prospect_id = sequence_context.get("prospect_id")
        related_ids = self._unique_non_blank(
            active_state_ids + active_sequence_ids + [str(prospect_id or ""), account_id, contact_id]
        )
        if prospect_id:
            fields_payload["outreach_prospect_id"] = str(prospect_id)

        self._pause_record_ids_by_reason.setdefault(reason_code, set()).add(record_id)
        self._route_record_ids_by_reason.setdefault(reason_code, set()).add(record_id)

        return EvidenceRow(
            record_id=record_id,
            sobject=sobject,
            fields=fields_payload,
            reason=reason,
            reason_code=reason_code,
            severity=self.definition.severity_default,
            related_ids=related_ids,
        )

    def _reason_for_signals(
        self,
        *,
        has_active_opp: bool,
        has_open_case: bool,
        has_recent_churn: bool,
    ) -> tuple[str, str]:
        if has_active_opp and has_open_case:
            return (
                "CUSTOMER_OPEN_TICKET_IN_SEQUENCE",
                "Record is in an active outbound sequence while account has an active opportunity and open support case.",
            )
        if has_active_opp:
            return (
                "EXISTING_CUSTOMER_IN_SEQUENCE",
                "Record is in an active outbound sequence while account has an active opportunity.",
            )
        if has_open_case:
            return (
                "OPEN_TICKET_IN_SEQUENCE",
                "Record is in an active outbound sequence while account/contact has an open support case.",
            )
        return (
            "RECENT_CHURN_IN_SEQUENCE",
            "Record is in an active outbound sequence during a recent churn re-engagement window.",
        )

    def _target_objects(self) -> list[str]:
        objects = ["Lead"]
        if bool(self.params.get("include_contacts", True)):
            objects.append("Contact")
        return objects

    def _base_filters_for_object(
        self,
        object_type: str,
        available_fields: set[str] | None = None,
    ) -> list[FilterExpr]:
        available = available_fields if available_fields is not None else self._available_fields(object_type)
        filters: list[FilterExpr] = [Filter("Email", FilterOp.IS_NOT_NULL)]
        lookback_days = max(0, self._to_int(self.params.get("lookback_days"), default=30))
        if lookback_days <= 0:
            return filters
        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 _fields_for_object(self, object_type: str, available_fields: set[str]) -> tuple[list[str], str | None]:
        fields: list[str] = ["Id"]
        for candidate in ("Name", "Email", "OwnerId", "AccountId", "Company"):
            if candidate.lower() in available_fields and candidate not in fields:
                fields.append(candidate)
        order_field = None
        if "lastmodifieddate" in available_fields:
            order_field = "LastModifiedDate"
        elif "createddate" in available_fields:
            order_field = "CreatedDate"
        if order_field and order_field not in fields:
            fields.append(order_field)
        return fields, order_field

    def _load_active_opportunity_accounts(self, account_ids: set[str]) -> set[str]:
        if not bool(self.params.get("include_active_opportunities", True)):
            return set()
        if not account_ids or not self.provider.supports_object("Opportunity"):
            return set()

        available = self._available_fields("Opportunity")
        if "accountid" not in available:
            self._warnings.append("Opportunity.AccountId unavailable; active-opportunity guardrail skipped.")
            return set()

        closed_stages = self.params.get("closed_opportunity_stages") or ["Closed Won", "Closed Lost"]
        lookback_days = max(0, self._to_int(self.params.get("opportunity_lookback_days"), default=365))
        matches: set[str] = set()
        for account_chunk in chunked(sorted(account_ids), size=200):
            filters: list[FilterExpr] = [Filter("AccountId", FilterOp.IN, list(account_chunk))]
            if "stagename" in available:
                filters.append(Filter("StageName", FilterOp.NOT_IN, list(closed_stages)))
            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))

            fields = ["AccountId"]
            if "stagename" in available:
                fields.append("StageName")
            rows = self.provider.query(
                "Opportunity",
                fields,
                filters,
                order_by="LastModifiedDate" if "lastmodifieddate" in available else None,
                order_dir="DESC",
                limit=max(self.top_n * 20, 200),
            )
            self._collect_provider_meta()
            for row in rows:
                account_id = str(row.get("AccountId") or "").strip()
                if account_id:
                    matches.add(account_id)
        return matches

    def _load_open_case_signals(
        self,
        account_ids: set[str],
        contact_ids: set[str],
    ) -> tuple[set[str], set[str]]:
        if not bool(self.params.get("include_open_tickets", True)):
            return set(), set()
        if not self.provider.supports_object("Case"):
            self._warnings.append("Case object unavailable; open-ticket guardrail skipped.")
            return set(), set()

        available = self._available_fields("Case")
        if "accountid" not in available and "contactid" not in available:
            self._warnings.append("Case.AccountId/Case.ContactId unavailable; open-ticket guardrail skipped.")
            return set(), set()

        case_lookback_days = max(0, self._to_int(self.params.get("case_lookback_days"), default=90))
        closed_case_status = str(self.params.get("closed_case_status") or "Closed").strip()
        open_accounts: set[str] = set()
        open_contacts: set[str] = set()

        keysets: list[tuple[str, set[str]]] = []
        if "accountid" in available and account_ids:
            keysets.append(("AccountId", account_ids))
        if "contactid" in available and contact_ids:
            keysets.append(("ContactId", contact_ids))
        if not keysets:
            return set(), set()

        for field_name, id_set in keysets:
            for id_chunk in chunked(sorted(id_set), size=200):
                filters: list[FilterExpr] = [Filter(field_name, FilterOp.IN, list(id_chunk))]
                if "status" in available:
                    filters.append(Filter("Status", FilterOp.NEQ, closed_case_status))
                if case_lookback_days > 0:
                    if "lastmodifieddate" in available:
                        filters.append(Filter("LastModifiedDate", FilterOp.LAST_N_DAYS, case_lookback_days))
                    elif "createddate" in available:
                        filters.append(Filter("CreatedDate", FilterOp.LAST_N_DAYS, case_lookback_days))

                fields = ["Status"]
                if "accountid" in available:
                    fields.append("AccountId")
                if "contactid" in available:
                    fields.append("ContactId")
                rows = self.provider.query(
                    "Case",
                    fields,
                    filters,
                    order_by="LastModifiedDate" if "lastmodifieddate" in available else None,
                    order_dir="DESC",
                    limit=max(self.top_n * 20, 200),
                )
                self._collect_provider_meta()
                for row in rows:
                    account_id = str(row.get("AccountId") or "").strip()
                    contact_id = str(row.get("ContactId") or "").strip()
                    if account_id:
                        open_accounts.add(account_id)
                    if contact_id:
                        open_contacts.add(contact_id)

        return open_accounts, open_contacts

    def _load_recent_churn_accounts(self, account_ids: set[str]) -> set[str]:
        if not bool(self.params.get("include_recent_churn", True)):
            return set()
        if not account_ids or not self.provider.supports_object("Account"):
            return set()

        available = self._available_fields("Account")
        configured = self.params.get("churn_fields") or ["IsChurned__c", "LastChurnDate__c"]
        churn_fields = [field for field in configured if str(field).lower() in available]
        if not churn_fields:
            self._warnings.append("No churn fields available on Account; recent-churn guardrail skipped.")
            return set()

        window_days = max(1, self._to_int(self.params.get("churn_window_days"), default=90))
        cutoff = datetime.now(timezone.utc) - timedelta(days=window_days)
        churned: set[str] = set()

        for account_chunk in chunked(sorted(account_ids), size=200):
            rows = self.provider.query(
                "Account",
                ["Id"] + churn_fields,
                [Filter("Id", FilterOp.IN, list(account_chunk))],
                limit=max(self.top_n * 20, 200),
            )
            self._collect_provider_meta()
            for row in rows:
                account_id = str(row.get("Id") or "").strip()
                if not account_id:
                    continue
                if self._row_has_recent_churn(row, churn_fields, cutoff):
                    churned.add(account_id)
        return churned

    def _row_has_recent_churn(
        self,
        row: dict[str, Any],
        churn_fields: list[str],
        cutoff: datetime,
    ) -> bool:
        for field_name in churn_fields:
            value = row.get(field_name)
            if value is None:
                continue
            if isinstance(value, bool):
                if value:
                    return True
                continue
            if isinstance(value, (int, float)):
                if value != 0:
                    return True
                continue
            if isinstance(value, str):
                text = value.strip()
                if not text:
                    continue
                if text.lower() in {"true", "1", "yes", "y"}:
                    return True
                parsed = self._parse_iso(text)
                if parsed is not None and parsed >= cutoff:
                    return True
        return False

    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; customer/open-ticket guardrail runs in no-op mode."
                )
                self._outreach_warning_emitted = True
            return None

    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))
                sequence_id = self._sequence_id_from_state(state)
                if sequence_id and sequence_id not in sequence_ids:
                    sequence_ids.append(sequence_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 _query_limit(self) -> int:
        default_limit = max(self.top_n * 5, 100)
        configured = self._to_int(self.params.get("max_candidates"), default=default_limit)
        return max(self.top_n, configured)

    def _extract_account_ids(self, rows: list[dict[str, Any]]) -> set[str]:
        account_ids: set[str] = set()
        for row in rows:
            account_id = str(row.get("AccountId") or "").strip()
            if account_id:
                account_ids.add(account_id)
        return account_ids

    def _extract_contact_ids(self, rows: list[dict[str, Any]]) -> set[str]:
        contact_ids: set[str] = set()
        for row in rows:
            contact_id = str(row.get("Id") or "").strip()
            if contact_id:
                contact_ids.add(contact_id)
        return contact_ids

    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")
        }

    @staticmethod
    def _attach_sobject(row: Dict[str, Any], object_type: str) -> Dict[str, Any]:
        copied = dict(row)
        copied["_sobject"] = object_type
        return copied

    @staticmethod
    def _parse_iso(value: Any) -> datetime | None:
        text = str(value or "").strip()
        if not text:
            return None
        try:
            parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
        except Exception:
            return None
        if parsed.tzinfo is None:
            return parsed.replace(tzinfo=timezone.utc)
        return parsed.astimezone(timezone.utc)

    @staticmethod
    def _unique_non_blank(values: list[str]) -> list[str]:
        ordered: list[str] = []
        seen: set[str] = set()
        for value in values:
            token = str(value or "").strip()
            if not token or token in seen:
                continue
            seen.add(token)
            ordered.append(token)
        return ordered

    @staticmethod
    def _to_int(value: Any, *, default: int) -> int:
        try:
            return int(value)
        except Exception:
            return default


__all__ = ["CustomerOpenTicketGuardrailCheck"]
