"""Integration health monitor (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 Filter, FilterExpr, FilterOp
from ..models import EvidenceRow, RecommendedAction, ScoutFinding
from ..utils import chunked, hours_since

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


class IntegrationHealthCheck(BaseScoutCheck):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self._login_supported = True

    def preflight(self) -> PreflightResult:
        if self.provider.name != "salesforce":
            return PreflightResult(ok=False, unsupported=True, reason="Integration health is Salesforce-only.")
        try:
            self.provider.describe("user")
            self.provider.describe("AsyncApexJob")
        except Exception as exc:
            return PreflightResult(ok=False, unsupported=True, reason=str(exc))
        try:
            if self.provider.supports_object("LoginHistory"):
                self.provider.describe("LoginHistory")
            else:
                self._login_supported = False
        except Exception as exc:
            self._login_supported = False
            self._warnings.append(f"LoginHistory unavailable; skipping auth checks. ({exc})")
        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)
            success_lookback_days = int(self.params.get("success_lookback_days", 30) or 30)
            baseline_window_days = int(self.params.get("baseline_window_days", lookback_days) or lookback_days)
            max_failure_records = int(self.params.get("max_failure_records", 200) or 200)
            spike_multiplier = float(self.params.get("spike_multiplier", 10) or 10)
            success_silence_hours = float(self.params.get("success_silence_hours", 48) or 48)
            include_login = bool(self.params.get("include_login_failures", True))
            include_api = bool(self.params.get("include_api_usage", False))
            api_threshold = float(self.params.get("api_utilization_threshold", 0.8) or 0.8)

            integration_users = self._resolve_integration_users()
            if not integration_users:
                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=0,
                    findings_count=0,
                    evidence=[],
                    recommended_actions=self.get_recommended_actions(),
                    soql_queries=self._soql_queries,
                    duration_ms=duration_ms,
                    parameters_used=self.params,
                    status="ok",
                    warnings=self._warnings + ["No integration users matched configuration."],
                )

            user_ids = list(integration_users.keys())

            failure_counts = self._query_failure_counts(user_ids, lookback_days)
            failure_rows = self._query_failure_rows(user_ids, lookback_days, max_failure_records)
            login_rows: List[Dict[str, Any]] = []
            if include_login and self._login_supported:
                login_rows = self._query_login_failures(user_ids, lookback_days)

            last_success = self._query_last_success(user_ids, success_lookback_days)
            baseline_counts: Dict[str, int] = {}
            if baseline_window_days > 0 and spike_multiplier > 0:
                baseline_counts = self._query_baseline_counts(user_ids, lookback_days, baseline_window_days)

            api_utilization_pct = None
            api_risk = False
            if include_api:
                api_utilization_pct = self._query_api_utilization()
                api_risk = api_utilization_pct is not None and api_utilization_pct >= api_threshold

            login_counts = self._count_by(login_rows, "UserId")
            top_errors = self._top_errors(failure_rows)
            top_job_types = self._top_job_types(failure_rows)

            evidence: List[EvidenceRow] = []
            total_failures = 0
            for user_id, user in integration_users.items():
                job_failures = failure_counts.get(user_id, 0)
                login_failures = login_counts.get(user_id, 0)
                total_fail = job_failures + login_failures
                total_failures += total_fail

                last_success_ts = last_success.get(user_id)
                hours_since_success = hours_since(last_success_ts) if last_success_ts else None
                no_recent_success = hours_since_success is None or hours_since_success > success_silence_hours

                baseline = baseline_counts.get(user_id, 0)
                spike = baseline > 0 and job_failures >= int(baseline * spike_multiplier)

                reason_codes = set()
                if login_failures > 0:
                    reason_codes.add("INTEGRATION_AUTH_FAILURE")
                if job_failures > 0 or spike or no_recent_success:
                    reason_codes.add("INTEGRATION_JOB_FAILURE_SPIKE")
                if api_risk:
                    reason_codes.add("API_LIMIT_RISK")

                if not reason_codes and total_fail == 0 and not api_risk:
                    continue

                severity = "high" if no_recent_success or spike or api_risk else "medium"

                reason = self._format_reason(reason_codes, no_recent_success, spike, job_failures)
                primary_code = self._primary_reason_code(reason_codes) or "INTEGRATION_HEALTH"

                fields = {
                    "user_id": user_id,
                    "username": user.get("Username"),
                    "name": user.get("Name"),
                    "email": user.get("Email"),
                    "profile": user.get("Profile.Name"),
                    "last_success_timestamp": last_success_ts,
                    "hours_since_last_success": hours_since_success,
                    "failure_count_window": total_fail,
                    "job_failure_count": job_failures,
                    "login_failure_count": login_failures,
                    "baseline_failure_count": baseline,
                    "spike_multiplier": spike_multiplier,
                    "top_error_types": top_errors.get(user_id, []),
                    "top_job_types": top_job_types.get(user_id, []),
                    "reason_codes": sorted(reason_codes),
                    "api_utilization_pct": api_utilization_pct,
                }

                evidence.append(
                    EvidenceRow(
                        record_id=user_id,
                        sobject="User",
                        fields=fields,
                        reason=reason,
                        reason_code=primary_code,
                        severity=severity,
                    )
                )

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

            status = "findings" if total_findings > 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_findings,
                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 _resolve_integration_users(self) -> Dict[str, Dict[str, Any]]:
        profiles = self._as_list(self.params.get("integration_profiles"))
        perm_sets = self._as_list(self.params.get("integration_permission_sets"))
        usernames = self._as_list(self.params.get("integration_usernames"))
        include_inactive = bool(self.params.get("include_inactive", False))
        infer_usernames = bool(self.params.get("infer_usernames", False))
        infer_keywords = self._as_list(self.params.get("infer_username_keywords"))

        user_ids: set[str] = set()
        users: Dict[str, Dict[str, Any]] = {}

        if usernames:
            filters = [Filter("Username", FilterOp.IN, usernames)]
            if not include_inactive:
                filters.append(Filter("IsActive", FilterOp.EQ, True))
            rows = self.provider.query(
                "user",
                ["Id", "Username", "Name", "Email", "IsActive", "Profile.Name"],
                filters,
                limit=max(200, len(usernames)),
            )
            self._collect_provider_meta()
            for row in rows:
                user_id = row.get("Id")
                if user_id:
                    user_ids.add(str(user_id))
                    users[str(user_id)] = row

        if profiles:
            filters = [Filter("Profile.Name", FilterOp.IN, profiles)]
            if not include_inactive:
                filters.append(Filter("IsActive", FilterOp.EQ, True))
            rows = self.provider.query(
                "user",
                ["Id", "Username", "Name", "Email", "IsActive", "Profile.Name"],
                filters,
                limit=500,
            )
            self._collect_provider_meta()
            for row in rows:
                user_id = row.get("Id")
                if user_id:
                    user_ids.add(str(user_id))
                    users[str(user_id)] = row

        if perm_sets:
            for chunk in chunked(list(perm_sets), size=100):
                escaped = ", ".join(f"'{sfdc_helpers.escape_soql_value(p)}'" for p in chunk)
                query = (
                    "SELECT AssigneeId "
                    "FROM PermissionSetAssignment "
                    f"WHERE PermissionSet.Name IN ({escaped})"
                )
                self._soql_queries.append(query)
                rows = sfdc_helpers.run_soql(query, org_alias=self.org_alias)
                for row in rows:
                    user_id = row.get("AssigneeId") or row.get("AssigneeId__c")
                    if user_id:
                        user_ids.add(str(user_id))

        if infer_usernames and infer_keywords:
            for keyword in infer_keywords:
                safe = sfdc_helpers.escape_soql_value(keyword)
                query = (
                    "SELECT Id, Username, Name, Email, IsActive, Profile.Name "
                    "FROM User "
                    f"WHERE Username LIKE '%{safe}%'"
                )
                if not include_inactive:
                    query += " AND IsActive = true"
                self._soql_queries.append(query)
                rows = sfdc_helpers.run_soql(query, org_alias=self.org_alias)
                for row in rows:
                    user_id = row.get("Id")
                    if user_id:
                        user_ids.add(str(user_id))
                        users[str(user_id)] = row

        missing = [user_id for user_id in user_ids if user_id not in users]
        if missing:
            for chunk in chunked(list(missing), size=200):
                filters = [Filter("Id", FilterOp.IN, chunk)]
                if not include_inactive:
                    filters.append(Filter("IsActive", FilterOp.EQ, True))
                rows = self.provider.query(
                    "user",
                    ["Id", "Username", "Name", "Email", "IsActive", "Profile.Name"],
                    filters,
                    limit=len(chunk),
                )
                self._collect_provider_meta()
                for row in rows:
                    user_id = row.get("Id")
                    if user_id:
                        users[str(user_id)] = row

        return users

    def _query_failure_counts(self, user_ids: List[str], lookback_days: int) -> Dict[str, int]:
        counts: Dict[str, int] = {}
        for chunk in chunked(list(user_ids), size=150):
            escaped = ", ".join(f"'{sfdc_helpers.escape_soql_value(i)}'" for i in chunk)
            query = (
                "SELECT CreatedById, COUNT(Id) FailureCount "
                "FROM AsyncApexJob "
                "WHERE Status IN ('Failed', 'Aborted') "
                f"AND CreatedDate >= LAST_N_DAYS:{lookback_days} "
                f"AND CreatedById IN ({escaped}) "
                "GROUP BY CreatedById"
            )
            self._soql_queries.append(query)
            rows = sfdc_helpers.run_soql(query, org_alias=self.org_alias)
            for row in rows:
                user_id = row.get("CreatedById")
                if not user_id:
                    continue
                counts[str(user_id)] = self._parse_aggregate_count(row, "FailureCount")
        return counts

    def _query_baseline_counts(
        self,
        user_ids: List[str],
        lookback_days: int,
        baseline_window_days: int,
    ) -> Dict[str, int]:
        counts: Dict[str, int] = {}
        if baseline_window_days <= 0:
            return counts
        upper = lookback_days
        lower = lookback_days + baseline_window_days
        for chunk in chunked(list(user_ids), size=150):
            escaped = ", ".join(f"'{sfdc_helpers.escape_soql_value(i)}'" for i in chunk)
            query = (
                "SELECT CreatedById, COUNT(Id) FailureCount "
                "FROM AsyncApexJob "
                "WHERE Status IN ('Failed', 'Aborted') "
                f"AND CreatedDate >= LAST_N_DAYS:{lower} "
                f"AND CreatedDate < LAST_N_DAYS:{upper} "
                f"AND CreatedById IN ({escaped}) "
                "GROUP BY CreatedById"
            )
            self._soql_queries.append(query)
            rows = sfdc_helpers.run_soql(query, org_alias=self.org_alias)
            for row in rows:
                user_id = row.get("CreatedById")
                if not user_id:
                    continue
                counts[str(user_id)] = self._parse_aggregate_count(row, "FailureCount")
        return counts

    def _query_failure_rows(
        self,
        user_ids: List[str],
        lookback_days: int,
        limit: int,
    ) -> List[Dict[str, Any]]:
        rows: List[Dict[str, Any]] = []
        fields = [
            "Id",
            "CreatedById",
            "Status",
            "JobType",
            "CreatedDate",
            "CompletedDate",
            "ExtendedStatus",
            "ApexClassId",
        ]
        for chunk in chunked(list(user_ids), size=150):
            filters: List[FilterExpr] = [
                Filter("CreatedById", FilterOp.IN, chunk),
                Filter("Status", FilterOp.IN, ["Failed", "Aborted"]),
                Filter("CreatedDate", FilterOp.LAST_N_DAYS, lookback_days),
            ]
            rows.extend(
                self.provider.query(
                    "AsyncApexJob",
                    fields,
                    filters,
                    order_by="CreatedDate",
                    order_dir="DESC",
                    limit=limit,
                )
            )
            self._collect_provider_meta()
            if limit and len(rows) >= limit:
                break
        return rows[:limit] if limit else rows

    def _query_login_failures(self, user_ids: List[str], lookback_days: int) -> List[Dict[str, Any]]:
        rows: List[Dict[str, Any]] = []
        fields = ["Id", "UserId", "Status", "LoginTime", "LoginType"]
        for chunk in chunked(list(user_ids), size=150):
            filters: List[FilterExpr] = [
                Filter("UserId", FilterOp.IN, chunk),
                Filter("Status", FilterOp.EQ, "Failed"),
                Filter("LoginTime", FilterOp.LAST_N_DAYS, lookback_days),
            ]
            rows.extend(
                self.provider.query(
                    "LoginHistory",
                    fields,
                    filters,
                    order_by="LoginTime",
                    order_dir="DESC",
                    limit=200,
                )
            )
            self._collect_provider_meta()
        return rows

    def _query_last_success(self, user_ids: List[str], lookback_days: int) -> Dict[str, str]:
        results: Dict[str, str] = {}
        for chunk in chunked(list(user_ids), size=150):
            escaped = ", ".join(f"'{sfdc_helpers.escape_soql_value(i)}'" for i in chunk)
            query = (
                "SELECT CreatedById, MAX(CompletedDate) LastSuccess "
                "FROM AsyncApexJob "
                "WHERE Status = 'Completed' "
                f"AND CreatedDate >= LAST_N_DAYS:{lookback_days} "
                f"AND CreatedById IN ({escaped}) "
                "GROUP BY CreatedById"
            )
            self._soql_queries.append(query)
            rows = sfdc_helpers.run_soql(query, org_alias=self.org_alias)
            for row in rows:
                user_id = row.get("CreatedById")
                last_success = row.get("LastSuccess")
                if user_id and last_success:
                    results[str(user_id)] = str(last_success)
        return results

    def _query_api_utilization(self) -> float | None:
        query = "SELECT DailyApiRequests, DailyApiLimit FROM Organization"
        self._soql_queries.append(query)
        try:
            rows = sfdc_helpers.run_soql(query, org_alias=self.org_alias)
        except Exception as exc:
            self._warnings.append(f"API usage query failed; skipping API risk. ({exc})")
            return None
        if not rows:
            return None
        row = rows[0]
        try:
            used = float(row.get("DailyApiRequests") or 0)
            limit = float(row.get("DailyApiLimit") or 0)
        except Exception:
            return None
        if limit <= 0:
            return None
        return used / limit

    def _top_errors(self, rows: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
        counts: Dict[str, Counter] = defaultdict(Counter)
        for row in rows:
            user_id = row.get("CreatedById")
            if not user_id:
                continue
            error = self._sanitize_error(row.get("ExtendedStatus") or row.get("Status"))
            counts[str(user_id)][error] += 1
        return {user_id: self._top_counts(counter) for user_id, counter in counts.items()}

    def _top_job_types(self, rows: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
        counts: Dict[str, Counter] = defaultdict(Counter)
        for row in rows:
            user_id = row.get("CreatedById")
            if not user_id:
                continue
            job_type = row.get("JobType") or row.get("ApexClassId") or "Unknown"
            counts[str(user_id)][str(job_type)] += 1
        return {user_id: self._top_counts(counter) for user_id, counter in counts.items()}

    def _top_counts(self, counter: Counter, limit: int = 3) -> List[Dict[str, Any]]:
        return [
            {"name": name, "count": count}
            for name, count in counter.most_common(limit)
        ]

    def _count_by(self, rows: List[Dict[str, Any]], key: str) -> Dict[str, int]:
        counts: Dict[str, int] = defaultdict(int)
        for row in rows:
            value = row.get(key)
            if not value:
                continue
            counts[str(value)] += 1
        return counts

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

    def _format_reason(
        self,
        codes: set[str],
        no_recent_success: bool,
        spike: bool,
        job_failures: int,
    ) -> str:
        messages: List[str] = []
        if "INTEGRATION_AUTH_FAILURE" in codes:
            messages.append("Authentication failures detected")
        if "INTEGRATION_JOB_FAILURE_SPIKE" in codes:
            if no_recent_success:
                threshold = int(self.params.get("success_silence_hours", 48) or 48)
                messages.append(f"No successful jobs in the last {threshold}h")
            elif spike:
                messages.append("Job failure spike detected")
            elif job_failures > 0:
                messages.append("Job failures detected")
            else:
                messages.append("Job failure spike detected")
        if "API_LIMIT_RISK" in codes:
            messages.append("API usage near limit")
        return "; ".join(messages) if messages else "Integration health warning"

    def _primary_reason_code(self, codes: set[str]) -> str | None:
        for code in ("API_LIMIT_RISK", "INTEGRATION_JOB_FAILURE_SPIKE", "INTEGRATION_AUTH_FAILURE"):
            if code in codes:
                return code
        return None

    def _parse_aggregate_count(self, row: Dict[str, Any], key: str) -> int:
        raw = row.get(key) or row.get("expr0") or row.get("count") or row.get("Count")
        try:
            return int(float(raw)) if raw is not None else 0
        except Exception:
            return 0

    def _as_list(self, value: Any) -> List[str]:
        if value is None:
            return []
        if isinstance(value, list):
            return [str(item) for item in value if item not in (None, "")]
        return [str(value)]

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

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

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

    def get_recommended_actions(self) -> List[RecommendedAction]:
        actions: List[RecommendedAction] = []
        if bool(self.params.get("slack_notify_enabled", False)):
            channel = self.params.get("slack_channel") or "#revops-alerts"
            actions.append(
                RecommendedAction(
                    action_type="slack.notify",
                    label=f"Alert {channel}",
                    risk_level="low",
                    details={
                        "channel": channel,
                        "severity": "high",
                        "title": "Integration Health Alert",
                    },
                )
            )

        details: Dict[str, Any] = {"subject": "Integration failure detected"}
        owner_id = self.params.get("task_owner_id")
        if owner_id:
            details["owner_id"] = owner_id
        actions.append(
            RecommendedAction(
                action_type="create_task",
                label="Create integration review task",
                risk_level="low",
                details=details,
            )
        )
        return actions


__all__ = ["IntegrationHealthCheck"]
