"""Workflow impact analysis (recommend-only)."""

from __future__ import annotations

import re
import time
from collections import Counter, defaultdict
from typing import Any, Dict, List, Tuple

from g_gremlin import sfdc as sfdc_helpers

from .base import BaseScoutCheck, PreflightResult
from ..crm_provider import FilterExpr
from ..models import EvidenceRow, RecommendedAction, ScoutFinding
from ..utils import parse_sf_datetime

_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")

_SOBJECT_PREFIX = {
    "001": "Account",
    "003": "Contact",
    "006": "Opportunity",
    "00Q": "Lead",
    "500": "Case",
    "00T": "Task",
    "005": "User",
}


class WorkflowImpactCheck(BaseScoutCheck):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self._flow_supported = True
        self._approval_supported = True
        self._flow_name_field: str | None = None
        self._flow_error_field: str | None = None
        self._flow_select_fields: List[str] = []

    def preflight(self) -> PreflightResult:
        if self.provider.name != "salesforce":
            return PreflightResult(ok=False, unsupported=True, reason="Workflow impact is Salesforce-only.")
        try:
            flow_desc = self.provider.describe("FlowInterview")
            flow_fields = self._extract_field_names(flow_desc)
            required = {"Id", "InterviewStatus", "CreatedDate"}
            missing = sorted(required - flow_fields)
            if missing:
                self._flow_supported = False
                self._warnings.append(
                    "FlowInterview missing required fields; skipping flow errors. "
                    f"({', '.join(missing)})"
                )
            else:
                self._flow_name_field = "FlowApiName" if "FlowApiName" in flow_fields else None
                if self._flow_name_field is None and "FlowVersionView" in flow_fields:
                    self._flow_name_field = "FlowVersionView"
                self._flow_error_field = "ErrorMessage" if "ErrorMessage" in flow_fields else None
                self._flow_select_fields = ["Id", "InterviewStatus", "CreatedDate"]
                if "CreatedById" in flow_fields:
                    self._flow_select_fields.append("CreatedById")
                if self._flow_name_field and self._flow_name_field not in self._flow_select_fields:
                    self._flow_select_fields.append(self._flow_name_field)
                if self._flow_error_field and self._flow_error_field not in self._flow_select_fields:
                    self._flow_select_fields.append(self._flow_error_field)
        except Exception as exc:
            self._flow_supported = False
            self._warnings.append(f"FlowInterview unavailable; skipping flow errors. ({exc})")
        try:
            self.provider.describe("ProcessInstanceWorkitem")
        except Exception as exc:
            self._approval_supported = False
            self._warnings.append(
                f"ProcessInstanceWorkitem unavailable; skipping approvals. ({exc})"
            )
        if not self._flow_supported and not self._approval_supported:
            return PreflightResult(ok=False, unsupported=True, reason="No workflow objects supported.")
        return PreflightResult(ok=True)

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

            lookback_days = int(self.params.get("lookback_days", 7) or 7)
            stuck_days = int(self.params.get("stuck_days", 7) or 7)
            max_flow_records = int(self.params.get("max_flow_records", 200) or 200)
            max_approval_records = int(self.params.get("max_approval_records", 200) or 200)
            include_flow = bool(self.params.get("include_flow_errors", True))
            include_approvals = bool(self.params.get("include_stuck_approvals", True))

            flow_rows: List[Dict[str, Any]] = []
            if include_flow and self._flow_supported:
                flow_rows = self._query_flow_errors(lookback_days, max_flow_records)

            approval_rows: List[Dict[str, Any]] = []
            if include_approvals and self._approval_supported:
                approval_rows = self._query_stuck_approvals(stuck_days, max_approval_records)

            evidence: List[EvidenceRow] = []
            total_failures = 0

            flow_evidence, flow_count = self._build_flow_evidence(flow_rows, lookback_days)
            approval_evidence, approval_count = self._build_approval_evidence(approval_rows, stuck_days)

            evidence.extend(flow_evidence)
            evidence.extend(approval_evidence)
            total_failures = flow_count + approval_count

            if len(evidence) > self.top_n:
                evidence = evidence[: self.top_n]

            status = "findings" if total_failures > 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_failures,
                findings_count=total_failures,
                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 _query_flow_errors(self, lookback_days: int, limit: int) -> List[Dict[str, Any]]:
        select_fields = ", ".join(self._flow_select_fields or ["Id", "InterviewStatus", "CreatedDate"])
        base_where = (
            "WHERE InterviewStatus = 'Error' "
            f"AND CreatedDate = LAST_N_DAYS:{lookback_days} "
        )
        error_filter = ""
        if self._flow_error_field:
            error_filter = f"AND {self._flow_error_field} LIKE '%FIELD_CUSTOM_VALIDATION_EXCEPTION%' "

        primary_query = (
            f"SELECT {select_fields} "
            "FROM FlowInterview "
            f"{base_where}{error_filter}"
            "ORDER BY CreatedDate DESC "
            f"LIMIT {int(limit)}"
        )
        self._soql_queries.append(primary_query)
        try:
            return sfdc_helpers.run_soql(primary_query, org_alias=self.org_alias)
        except Exception as exc:
            if not self._flow_error_field:
                raise
            self._warnings.append(f"FlowInterview query fallback used. ({exc})")

        fallback_query = (
            f"SELECT {select_fields} "
            "FROM FlowInterview "
            f"{base_where}"
            "ORDER BY CreatedDate DESC "
            f"LIMIT {int(limit)}"
        )
        self._soql_queries.append(fallback_query)
        rows = sfdc_helpers.run_soql(fallback_query, org_alias=self.org_alias)
        if not self._flow_error_field:
            return rows
        filtered: List[Dict[str, Any]] = []
        for row in rows:
            message = str(row.get(self._flow_error_field) or "")
            if "FIELD_CUSTOM_VALIDATION_EXCEPTION" in message:
                filtered.append(row)
        return filtered

    def _query_stuck_approvals(self, stuck_days: int, limit: int) -> List[Dict[str, Any]]:
        rich_query = (
            "SELECT Id, ProcessInstanceId, ActorId, CreatedDate, "
            "ProcessInstance.TargetObjectId, ProcessInstance.ProcessDefinition.Name "
            "FROM ProcessInstanceWorkitem "
            f"WHERE CreatedDate < LAST_N_DAYS:{stuck_days} "
            "AND IsDeleted = false "
            "ORDER BY CreatedDate ASC "
            f"LIMIT {int(limit)}"
        )
        self._soql_queries.append(rich_query)
        try:
            rows = sfdc_helpers.run_soql(rich_query, org_alias=self.org_alias)
        except Exception as exc:
            self._warnings.append(f"ProcessInstanceWorkitem query fallback used. ({exc})")
            fallback_query = (
                "SELECT Id, ProcessInstanceId, ActorId, CreatedDate "
                "FROM ProcessInstanceWorkitem "
                f"WHERE CreatedDate < LAST_N_DAYS:{stuck_days} "
                "ORDER BY CreatedDate ASC "
                f"LIMIT {int(limit)}"
            )
            self._soql_queries.append(fallback_query)
            rows = sfdc_helpers.run_soql(fallback_query, org_alias=self.org_alias)

        # Deduplicate by ProcessInstanceId to avoid spam
        seen: set[str] = set()
        unique: List[Dict[str, Any]] = []
        for row in rows:
            pid = row.get("ProcessInstanceId")
            if not pid:
                continue
            if pid in seen:
                continue
            seen.add(pid)
            unique.append(row)
        return unique

    def _build_flow_evidence(
        self, rows: List[Dict[str, Any]], lookback_days: int
    ) -> Tuple[List[EvidenceRow], int]:
        if not rows:
            return [], 0
        reason_code = "VALIDATION_RULE_BLOCKING" if self._flow_error_field else "FLOW_FAILURES"
        error_type_label = (
            "FIELD_CUSTOM_VALIDATION_EXCEPTION" if self._flow_error_field else "FLOW_INTERVIEW_ERROR"
        )
        groups: Dict[Tuple[str, str], List[Dict[str, Any]]] = defaultdict(list)
        for row in rows:
            flow_name = row.get(self._flow_name_field or "") if self._flow_name_field else None
            flow_name = flow_name or "Flow"
            groups[(str(flow_name), reason_code)].append(row)

        evidence: List[EvidenceRow] = []
        total = 0
        for (flow_name, error_type), group_rows in groups.items():
            total += len(group_rows)
            affected_users = {row.get("CreatedById") for row in group_rows if row.get("CreatedById")}
            affected_records = {row.get("Id") for row in group_rows if row.get("Id")}
            error_samples = self._top_error_samples(group_rows)
            distinct_errors = {
                self._sanitize_error(row.get(self._flow_error_field or "") or "")
                for row in group_rows
                if self._flow_error_field and row.get(self._flow_error_field)
            }
            distinct_errors.discard("")
            failures_per_day = len(group_rows) / max(1, lookback_days)
            severity = "high" if failures_per_day > 50 else "medium"
            evidence.append(
                EvidenceRow(
                    record_id=f"flow:{flow_name}",
                    sobject="FlowInterview",
                    fields={
                        "object_type": flow_name,
                        "error_type": error_type_label,
                        "failure_count": len(group_rows),
                        "affected_user_count": len(affected_users),
                        "affected_record_count": len(group_rows),
                        "error_samples": error_samples,
                        "affected_object": flow_name,
                        "error_count": len(distinct_errors) or len(group_rows),
                        "affected_users_count": len(affected_users),
                        "affected_records_count": len(affected_records),
                        "sample_errors": error_samples,
                        "sample_records": [row.get("Id") for row in group_rows[:3] if row.get("Id")],
                    },
                    reason=f"Flow failures detected for {flow_name}",
                    reason_code=reason_code,
                    severity=severity,
                )
            )
        return evidence, total

    def _build_approval_evidence(
        self, rows: List[Dict[str, Any]], stuck_days: int
    ) -> Tuple[List[EvidenceRow], int]:
        if not rows:
            return [], 0
        groups: Dict[Tuple[str, str], List[Dict[str, Any]]] = defaultdict(list)
        for row in rows:
            target_id = row.get("ProcessInstance.TargetObjectId") or ""
            obj_type = self._infer_object_type(target_id)
            groups[(obj_type, "APPROVAL_STUCK")].append(row)

        evidence: List[EvidenceRow] = []
        total = 0
        for (obj_type, error_type), group_rows in groups.items():
            total += len(group_rows)
            affected_users = {row.get("ActorId") for row in group_rows if row.get("ActorId")}
            target_ids = [
                row.get("ProcessInstance.TargetObjectId")
                for row in group_rows
                if row.get("ProcessInstance.TargetObjectId")
            ]
            process_names = [
                row.get("ProcessInstance.ProcessDefinition.Name")
                for row in group_rows
                if row.get("ProcessInstance.ProcessDefinition.Name")
            ]
            process_name = self._most_common(process_names) or "Approval Process"
            oldest_stuck = self._oldest_date(group_rows)
            failures_per_day = len(group_rows) / max(1, stuck_days)
            severity = "high" if failures_per_day > 50 else "medium"
            evidence.append(
                EvidenceRow(
                    record_id=f"approval:{obj_type}",
                    sobject="ProcessInstanceWorkitem",
                    fields={
                        "object_type": obj_type,
                        "error_type": error_type,
                        "stuck_count": len(group_rows),
                        "affected_user_count": len(affected_users),
                        "affected_record_count": len(set(target_ids)),
                        "process_name": process_name,
                        "oldest_stuck_date": oldest_stuck,
                        "affected_users_count": len(affected_users),
                        "affected_records_count": len(set(target_ids)),
                        "sample_records": [row.get("Id") for row in group_rows[:3] if row.get("Id")],
                        "sample_target_ids": target_ids[:3],
                    },
                    reason=f"Approvals stuck longer than {stuck_days} days",
                    reason_code="APPROVAL_STUCK",
                    severity=severity,
                )
            )
        return evidence, total

    def _top_error_samples(self, rows: List[Dict[str, Any]], limit: int = 3) -> List[str]:
        if not self._flow_error_field:
            return []
        counter: Counter = Counter()
        for row in rows:
            msg = self._sanitize_error(row.get(self._flow_error_field) or "")
            if msg:
                counter[msg] += 1
        return [msg for msg, _ in counter.most_common(limit)]

    def _sanitize_error(self, value: Any) -> str:
        text = str(value or "").strip()
        if not text:
            return ""
        text = _EMAIL_RE.sub("[redacted]", text)
        text = re.sub(r"\s+", " ", text)
        if len(text) > 140:
            text = text[:137] + "..."
        return text

    def _most_common(self, values: List[str]) -> str | None:
        if not values:
            return None
        counter = Counter(values)
        return counter.most_common(1)[0][0]

    def _oldest_date(self, rows: List[Dict[str, Any]]) -> str | None:
        oldest = None
        oldest_raw = None
        for row in rows:
            raw = row.get("CreatedDate")
            dt = parse_sf_datetime(raw)
            if not dt:
                continue
            if oldest is None or dt < oldest:
                oldest = dt
                oldest_raw = raw
        return oldest_raw

    def _infer_object_type(self, record_id: str) -> str:
        if not record_id:
            return "Record"
        prefix = record_id[:3]
        return _SOBJECT_PREFIX.get(prefix, "Record")

    def _extract_field_names(self, desc: Dict[str, Any]) -> set[str]:
        if "result" in desc and isinstance(desc.get("result"), dict):
            fields = desc.get("result", {}).get("fields", []) or []
            return {f.get("name") for f in fields if isinstance(f, dict) and f.get("name")}
        fields = desc.get("fields", []) if isinstance(desc, dict) else []
        if isinstance(fields, list):
            return {f.get("name") for f in fields if isinstance(f, dict) and f.get("name")}
        return set()

    def build_count_filters(self) -> Tuple[str, List[FilterExpr]]:
        return "FlowInterview", []

    def build_sample_request(self) -> Tuple[str, List[str], List[FilterExpr], str | None, str, bool]:
        return "FlowInterview", ["Id"], [], None, "ASC", False

    def evaluate(self, rows: List[Dict[str, Any]]) -> List[EvidenceRow]:
        return []

    def get_recommended_actions(self) -> List[RecommendedAction]:
        details: Dict[str, Any] = {"subject": "Workflow impact detected"}
        owner_id = self.params.get("task_owner_id")
        if owner_id:
            details["owner_id"] = owner_id
        return [
            RecommendedAction(
                action_type="create_task",
                label="Create workflow impact review task",
                risk_level="low",
                details=details,
            )
        ]


__all__ = ["WorkflowImpactCheck"]
