"""Sequence collision scout check."""

from __future__ import annotations

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

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

if TYPE_CHECKING:
    from g_gremlin.outreach import OutreachClient


class SequenceCollisionCheck(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._recommended_actions: list[RecommendedAction] = []
        self._action_keys: set[tuple[str, str, str]] = set()

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

        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; collision scan 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._recommended_actions = []
        self._action_keys = set()
        self._sequence_lookup_cache = {}

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

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

            self._outreach_client = self._build_outreach_client()

            total_scanned = 0
            full_evidence: list[EvidenceRow] = []
            query_limit = self._query_limit()

            for object_type in target_objects:
                if not self.provider.supports_object(object_type):
                    continue

                self._current_object = object_type
                available = self._available_fields(object_type)
                if "email" not in available:
                    continue

                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

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

                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]

                if self._outreach_client is None:
                    continue
                full_evidence.extend(self.evaluate(typed_rows))

            findings_count = len(full_evidence)
            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]:
        return list(self._recommended_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

        context = self._lookup_sequence_context(email)
        active_states = list(context.get("active_sequence_states", []))
        min_active = max(2, self._to_int(self.params.get("min_active_sequences"), default=2))
        if len(active_states) < min_active:
            return None

        keeper, pause_candidates, priority_reason = self._select_priority(active_states)
        if keeper is None or not pause_candidates:
            return None

        active_state_ids = [str(item.get("sequence_state_id") or "") for item in active_states]
        active_sequence_ids = [str(item.get("sequence_id") or "") for item in active_states if item.get("sequence_id")]
        pause_state_ids = [str(item.get("sequence_state_id") or "") for item in pause_candidates]
        pause_sequence_ids = [str(item.get("sequence_id") or "") for item in pause_candidates if item.get("sequence_id")]

        sobject = str(row.get("_sobject") or self._current_object or "Lead")
        reason = (
            f"Prospect is enrolled in {len(active_states)} active sequences; "
            f"keep one enrollment and pause the lower-priority overlaps."
        )
        fields_payload: dict[str, Any] = {
            "Name": row.get("Name"),
            "Email": email,
            "OwnerId": row.get("OwnerId"),
            "active_sequence_count": len(active_states),
            "active_sequence_ids": active_sequence_ids,
            "active_sequence_state_ids": active_state_ids,
            "keeper_sequence_state_id": keeper.get("sequence_state_id"),
            "keeper_sequence_id": keeper.get("sequence_id"),
            "keeper_sequence_name": keeper.get("sequence_name"),
            "pause_sequence_state_ids": pause_state_ids,
            "pause_sequence_ids": pause_sequence_ids,
            "priority_reason": priority_reason,
        }

        prospect_id = context.get("prospect_id")
        related_ids = self._dedupe_non_blank(active_state_ids + active_sequence_ids + [str(prospect_id or "")])
        if prospect_id:
            fields_payload["outreach_prospect_id"] = str(prospect_id)

        self._add_action(
            record_id=record_id,
            sobject=sobject,
            prospect_id=str(prospect_id or ""),
            email=email,
            keeper=keeper,
            pause_candidates=pause_candidates,
            priority_reason=priority_reason,
        )

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

    def _add_action(
        self,
        *,
        record_id: str,
        sobject: str,
        prospect_id: str,
        email: str,
        keeper: dict[str, Any],
        pause_candidates: list[dict[str, Any]],
        priority_reason: str,
    ) -> None:
        keeper_state_id = str(keeper.get("sequence_state_id") or "")
        pause_state_ids = [str(item.get("sequence_state_id") or "") for item in pause_candidates]
        pause_state_ids = [value for value in pause_state_ids if value]
        key_anchor = prospect_id or email
        action_key = (key_anchor, keeper_state_id, ",".join(sorted(pause_state_ids)))
        if not key_anchor or not keeper_state_id or not pause_state_ids or action_key in self._action_keys:
            return

        self._action_keys.add(action_key)
        pause_sequence_ids = [str(item.get("sequence_id") or "") for item in pause_candidates if item.get("sequence_id")]
        self._recommended_actions.append(
            RecommendedAction(
                action_type="pause_sequence",
                label="Pause lower-priority sequence enrollment",
                risk_level="low",
                details={
                    "reason_code": "SEQUENCE_COLLISION",
                    "record_id": record_id,
                    "sobject": sobject,
                    "prospect_id": key_anchor,
                    "keep_sequence_state_id": keeper_state_id,
                    "keep_sequence_id": keeper.get("sequence_id"),
                    "pause_sequence_state_ids": pause_state_ids,
                    "pause_sequence_ids": pause_sequence_ids,
                    "priority_reason": priority_reason,
                },
            )
        )

    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; sequence collision scan 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_states": [],
        }
        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)
            active_states: list[dict[str, Any]] = []
            for item in states:
                parsed = self._parse_active_sequence_state(item)
                if parsed is not None:
                    active_states.append(parsed)

            resolved = {
                "outreach_connected": True,
                "prospect_id": prospect_id,
                "active_sequence_states": active_states,
            }
            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 _parse_active_sequence_state(self, state: Any) -> dict[str, Any] | None:
        if not isinstance(state, dict):
            return None
        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:
            return None

        state_id = str(state.get("id") or "").strip()
        if not state_id:
            return None

        sequence_id = self._sequence_id_from_state(state)
        created_at = self._extract_state_timestamp(attributes)
        return {
            "sequence_state_id": state_id,
            "sequence_id": sequence_id,
            "sequence_name": self._sequence_name_from_state(state),
            "state": state_value,
            "created_at": created_at,
        }

    def _sequence_id_from_state(self, state: dict[str, Any]) -> str:
        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 {}
        for key in ("sequenceId", "sequence_id"):
            value = attributes.get(key)
            if value:
                return str(value)
        return ""

    def _sequence_name_from_state(self, state: dict[str, Any]) -> str:
        attributes = state.get("attributes") if isinstance(state.get("attributes"), dict) else {}
        for key in ("sequenceName", "sequence_name", "name"):
            value = attributes.get(key)
            if value:
                return str(value)
        return ""

    def _extract_state_timestamp(self, attributes: dict[str, Any]) -> str:
        for key in ("createdAt", "created_at", "createdDate", "created"):
            value = attributes.get(key)
            if value:
                return str(value)
        return ""

    def _select_priority(
        self, active_states: list[dict[str, Any]]
    ) -> tuple[dict[str, Any] | None, list[dict[str, Any]], str]:
        def sort_key(item: dict[str, Any]) -> tuple[float, str, str]:
            created_at = self._parse_iso_datetime(item.get("created_at"))
            created_rank = created_at.timestamp() if created_at is not None else float("inf")
            sequence_id = str(item.get("sequence_id") or "")
            state_id = str(item.get("sequence_state_id") or "")
            return (created_rank, sequence_id, state_id)

        ordered = sorted(active_states, key=sort_key)
        if not ordered:
            return None, [], "stable_id_order"
        keeper = ordered[0]
        reason = "oldest_active_enrollment"
        if self._parse_iso_datetime(keeper.get("created_at")) is None:
            reason = "stable_id_order"
        return keeper, ordered[1:], reason

    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 _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 _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 _parse_iso_datetime(self, 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)

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

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

    def _dedupe_non_blank(self, 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


__all__ = ["SequenceCollisionCheck"]
