"""License utilization scout (recommend-only)."""

from __future__ import annotations

import time
from collections import 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, OrGroup
from ..models import EvidenceRow, RecommendedAction, ScoutFinding
from ..utils import chunked


class LicenseUtilizationCheck(BaseScoutCheck):
    def preflight(self) -> PreflightResult:
        if self.provider.name != "salesforce":
            return PreflightResult(ok=False, unsupported=True, reason="License utilization is Salesforce-only.")
        try:
            self.provider.describe("user")
        except Exception as exc:
            return PreflightResult(ok=False, unsupported=True, reason=str(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", 90) or 90)
            max_users = int(self.params.get("max_users", 500) or 500)
            exclude_profiles = self._as_list(self.params.get("exclude_profiles"))
            exclude_perm_sets = self._as_list(self.params.get("exclude_permission_sets"))
            exclude_usernames = self._as_list(self.params.get("exclude_usernames"))
            license_cost = float(self.params.get("license_cost_per_user", 0) or 0)
            activity_objects = self._as_list(self.params.get("activity_objects"))

            candidate_users = self._query_candidate_users(
                lookback_days,
                max_users,
                exclude_profiles,
                exclude_usernames,
            )
            if not candidate_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,
                )

            user_map = {row.get("Id"): row for row in candidate_users if row.get("Id")}
            user_ids = list(user_map.keys())
            if exclude_perm_sets:
                excluded_by_perm = self._exclude_by_permission_sets(user_ids, exclude_perm_sets)
                user_ids = [uid for uid in user_ids if uid not in excluded_by_perm]

            if not user_ids:
                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,
                )

            created_counts, modified_counts = self._query_activity_counts(
                user_ids,
                activity_objects,
                lookback_days,
            )

            evidence: List[EvidenceRow] = []
            for user_id in user_ids:
                row = user_map.get(user_id, {})
                created = created_counts.get(user_id, 0)
                modified = modified_counts.get(user_id, 0)
                if created > 0 or modified > 0:
                    continue

                last_login = row.get("LastLoginDate")
                manager_id = row.get("ManagerId")
                manager_name = row.get("Manager.Name")
                fields = {
                    "user_id": user_id,
                    "username": row.get("Username"),
                    "name": row.get("Name"),
                    "email": row.get("Email"),
                    "profile": row.get("Profile.Name"),
                    "license_type": row.get("Profile.Name") or row.get("UserType"),
                    "last_login_date": last_login,
                    "records_created_count": created,
                    "records_modified_count": modified,
                    "estimated_cost_per_month": license_cost,
                    "manager_id": manager_id,
                    "manager_name": manager_name,
                }
                if manager_id:
                    fields["OwnerId"] = manager_id

                evidence.append(
                    EvidenceRow(
                        record_id=user_id,
                        sobject="User",
                        fields=fields,
                        reason=f"No login or activity in last {lookback_days} days",
                        reason_code="UNUSED_LICENSE",
                        severity="medium",
                    )
                )

            total_flagged = len(evidence)
            summary_row = None
            if license_cost > 0 and total_flagged > 0:
                total_savings = total_flagged * license_cost
                summary_row = EvidenceRow(
                    record_id="summary",
                    sobject="User",
                    fields={
                        "total_flagged_users": total_flagged,
                        "license_cost_per_user": license_cost,
                        "estimated_total_savings_per_month": total_savings,
                        "estimated_annual_savings": total_savings * 12,
                    },
                    reason="License utilization savings estimate",
                    reason_code="TOTAL_SAVINGS_ESTIMATE",
                    severity="info",
                )

            status = "findings" if evidence else "ok"
            duration_ms = int((time.perf_counter() - start) * 1000)
            if summary_row is not None:
                evidence_out = [summary_row] + evidence[: self.top_n]
            else:
                evidence_out = evidence[: self.top_n]
            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=len(user_ids),
                findings_count=total_flagged,
                evidence=evidence_out,
                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_candidate_users(
        self,
        lookback_days: int,
        max_users: int,
        exclude_profiles: List[str],
        exclude_usernames: List[str],
    ) -> List[Dict[str, Any]]:
        filters: List[FilterExpr] = [
            Filter("IsActive", FilterOp.EQ, True),
            OrGroup(
                filters=[
                    Filter("LastLoginDate", FilterOp.IS_NULL),
                    Filter("LastLoginDate", FilterOp.NOT_LAST_N_DAYS, lookback_days),
                ]
            ),
        ]
        if exclude_profiles:
            filters.append(Filter("Profile.Name", FilterOp.NOT_IN, exclude_profiles))
        if exclude_usernames:
            filters.append(Filter("Username", FilterOp.NOT_IN, exclude_usernames))

        fields = [
            "Id",
            "Name",
            "Username",
            "Email",
            "IsActive",
            "Profile.Name",
            "UserType",
            "LastLoginDate",
            "ManagerId",
            "Manager.Name",
        ]
        rows = self.provider.query("user", fields, filters, order_by="LastLoginDate", order_dir="ASC", limit=max_users)
        self._collect_provider_meta()
        if not rows:
            return []
        filtered = []
        for row in rows:
            profile_name = row.get("Profile.Name")
            username = row.get("Username")
            if profile_name and profile_name in exclude_profiles:
                continue
            if username and username in exclude_usernames:
                continue
            filtered.append(row)
        return filtered

    def _exclude_by_permission_sets(self, user_ids: List[str], perm_sets: List[str]) -> set[str]:
        excluded: set[str] = set()
        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:
                assignee = row.get("AssigneeId")
                if assignee:
                    excluded.add(str(assignee))
        return excluded

    def _query_activity_counts(
        self,
        user_ids: List[str],
        objects: List[str],
        lookback_days: int,
    ) -> Tuple[Dict[str, int], Dict[str, int]]:
        created_counts: Dict[str, int] = defaultdict(int)
        modified_counts: Dict[str, int] = defaultdict(int)
        if not user_ids:
            return created_counts, modified_counts

        for obj in objects:
            if not self.provider.supports_object(obj):
                self._warnings.append(f"Unsupported object for activity check: {obj}")
                continue
            for chunk in chunked(list(user_ids), size=200):
                escaped_ids = ", ".join(f"'{sfdc_helpers.escape_soql_value(i)}'" for i in chunk)
                created_query = (
                    f"SELECT OwnerId, COUNT(Id) cnt FROM {obj} "
                    f"WHERE OwnerId IN ({escaped_ids}) "
                    f"AND CreatedDate = LAST_N_DAYS:{lookback_days} "
                    "GROUP BY OwnerId"
                )
                self._soql_queries.append(created_query)
                created_rows = sfdc_helpers.run_soql(created_query, org_alias=self.org_alias)
                for row in created_rows:
                    owner_id = row.get("OwnerId")
                    if owner_id:
                        created_counts[str(owner_id)] += self._parse_count(row)

                modified_query = (
                    f"SELECT OwnerId, COUNT(Id) cnt FROM {obj} "
                    f"WHERE OwnerId IN ({escaped_ids}) "
                    f"AND LastModifiedDate = LAST_N_DAYS:{lookback_days} "
                    "GROUP BY OwnerId"
                )
                self._soql_queries.append(modified_query)
                modified_rows = sfdc_helpers.run_soql(modified_query, org_alias=self.org_alias)
                for row in modified_rows:
                    owner_id = row.get("OwnerId")
                    if owner_id:
                        modified_counts[str(owner_id)] += self._parse_count(row)

        return created_counts, modified_counts

    def _parse_count(self, row: Dict[str, Any]) -> int:
        raw = row.get("cnt") 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 "user", []

    def build_sample_request(self) -> Tuple[str, List[str], List[FilterExpr], str | None, str, bool]:
        return "user", ["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": "Review inactive user license"}
        owner_id = self.params.get("task_owner_id")
        if owner_id:
            details["owner_id"] = owner_id
        return [
            RecommendedAction(
                action_type="create_task",
                label="Create manager review tasks",
                risk_level="low",
                details=details,
            ),
            RecommendedAction(
                action_type="deactivate_user",
                label="Deactivate inactive users",
                risk_level="medium",
            ),
            RecommendedAction(
                action_type="export_csv",
                label="Export inactive users to CSV",
                risk_level="safe",
            ),
        ]


__all__ = ["LicenseUtilizationCheck"]
