"""Base classes for Scout checks."""

from __future__ import annotations

import abc
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Tuple

from ..crm_provider import CRMProvider, FilterExpr
from ..models import EvidenceRow, RecommendedAction, ScoutDefinition, ScoutFinding


@dataclass
class PreflightResult:
    ok: bool = True
    unsupported: bool = False
    reason: str | None = None


class BaseScoutCheck(abc.ABC):
    """Base class for Scout checks with count + sample query pattern."""

    def __init__(
        self,
        definition: ScoutDefinition,
        params: Dict[str, Any],
        top_n: int,
        org_alias: str | None = None,
        provider: CRMProvider | None = None,
        state_dir: Path | str | None = None,
    ) -> None:
        self.definition = definition
        self.params = params
        self.top_n = top_n
        self.org_alias = org_alias
        if provider is None:
            raise ValueError("provider is required for Scout checks")
        self.provider = provider
        self.state_dir = Path(state_dir).resolve() if state_dir is not None else None
        self._soql_queries: List[str] = []
        self._warnings: List[str] = []

    def preflight(self) -> PreflightResult:
        sobject = self._primary_sobject()
        if not sobject:
            return PreflightResult(ok=True)
        if not self.provider.supports_object(sobject):
            return PreflightResult(ok=False, unsupported=True, reason=f"Unsupported object: {sobject}")
        try:
            self.provider.describe(sobject)
            self._collect_provider_meta()
        except Exception as exc:
            return PreflightResult(ok=False, unsupported=True, reason=str(exc))

        requirements = self.definition.field_requirements or {}
        for object_type, required_fields in requirements.items():
            requirement_result = self._require_fields(object_type, list(required_fields))
            if requirement_result is not None:
                return requirement_result
        return PreflightResult(ok=True)

    def execute(self) -> ScoutFinding:
        start = time.perf_counter()
        try:
            preflight = self.preflight()
            if preflight.unsupported:
                return self._unsupported_finding(preflight.reason or "Unsupported")

            obj_type, filters = self.build_count_filters()
            count = self.provider.count(obj_type, filters)
            self._collect_provider_meta()
            evidence: List[EvidenceRow] = []
            if count > 0:
                rows = self.fetch_sample_rows()
                evidence = self.evaluate(rows)
                if len(evidence) > self.top_n:
                    evidence = evidence[: self.top_n]
            status = self._derive_status(count, evidence)
            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=count,
                findings_count=self._findings_count(count, evidence),
                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 _primary_sobject(self) -> str | None:
        if self.definition.sobjects_read:
            return self.definition.sobjects_read[0]
        return None

    def _findings_count(self, count: int, evidence: List[EvidenceRow]) -> int:
        return count

    def _derive_status(self, count: int, evidence: List[EvidenceRow]) -> str:
        return "findings" if count > 0 else "ok"

    def fetch_sample_rows(self) -> List[Dict[str, Any]]:
        obj_type, fields, filters, order_by, order_dir, nulls_first = self.build_sample_request()
        rows = self.provider.query(
            obj_type,
            fields,
            filters,
            order_by=order_by,
            order_dir=order_dir,
            nulls_first=nulls_first,
            limit=self.top_n,
        )
        self._collect_provider_meta()
        return rows

    def _collect_provider_meta(self) -> None:
        try:
            self._warnings.extend(self.provider.drain_warnings())
            self._soql_queries.extend(self.provider.drain_queries())
        except Exception:
            return

    def _require_fields(
        self,
        object_type: str,
        required_fields: List[str],
        *,
        require_access: bool = True,
    ) -> PreflightResult | None:
        if not required_fields:
            return None
        if not self.provider.supports_object(object_type):
            return PreflightResult(
                ok=False,
                unsupported=True,
                reason=f"Unsupported object: {object_type}",
            )

        try:
            present, accessible = self._field_sets(object_type)
        except Exception as exc:
            return PreflightResult(ok=False, unsupported=True, reason=str(exc))

        missing: List[str] = []
        blocked: List[str] = []
        for field in required_fields:
            key = str(field or "").strip().lower()
            if not key:
                continue
            if key not in present:
                missing.append(field)
            elif require_access and key not in accessible:
                blocked.append(field)

        if not missing and not blocked:
            return None

        detail_parts: List[str] = []
        if missing:
            detail_parts.append(f"missing fields: {', '.join(sorted(missing))}")
        if blocked:
            detail_parts.append(f"inaccessible fields: {', '.join(sorted(blocked))}")
        detail = "; ".join(detail_parts)
        return PreflightResult(
            ok=False,
            unsupported=True,
            reason=f"{object_type} capability check failed ({detail})",
        )

    def _field_sets(self, object_type: str) -> tuple[set[str], set[str]]:
        meta_method = getattr(self.provider, "field_metadata", None)
        if callable(meta_method):
            metadata = meta_method(object_type) or {}
            present = {str(name).lower() for name in metadata.keys()}
            accessible = {
                str(name).lower()
                for name, item in metadata.items()
                if bool((item or {}).get("accessible", True))
            }
            return present, accessible

        describe_payload = self.provider.describe(object_type)
        fields = self._extract_fields_from_describe(describe_payload)
        present: set[str] = set()
        accessible: set[str] = set()
        for item in fields:
            if not isinstance(item, dict):
                continue
            name = str(item.get("name") or "").strip()
            if not name:
                continue
            lowered = name.lower()
            present.add(lowered)
            if bool(item.get("accessible", True)):
                accessible.add(lowered)
        return present, accessible

    def _extract_fields_from_describe(self, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
        if not isinstance(payload, dict):
            return []
        result = payload.get("result")
        if isinstance(result, dict):
            fields = result.get("fields")
            if isinstance(fields, list):
                return [item for item in fields if isinstance(item, dict)]
        fields = payload.get("fields")
        if isinstance(fields, list):
            return [item for item in fields if isinstance(item, dict)]
        properties = payload.get("properties")
        if isinstance(properties, list):
            return [item for item in properties if isinstance(item, dict)]
        return []

    def _parse_count_rows(self, rows: List[Dict[str, str]]) -> int:
        if not rows:
            return 0
        row = rows[0]
        for key in ("expr0", "count", "total", "totalcount", "cnt"):
            if key in row:
                try:
                    return int(float(row[key]))
                except Exception:
                    return 0
        # Fallback: use first column value
        try:
            return int(float(next(iter(row.values()))))
        except Exception:
            return 0

    def get_recommended_actions(self) -> List[RecommendedAction]:
        return []

    def _unsupported_finding(self, reason: str) -> ScoutFinding:
        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=0,
            parameters_used=self.params,
            status="unsupported",
            status_detail=reason,
        )

    def _error_finding(self, error: str, duration_ms: int = 0) -> ScoutFinding:
        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="error",
            status_detail=error,
        )

    @abc.abstractmethod
    def build_count_filters(self) -> Tuple[str, List[FilterExpr]]:
        raise NotImplementedError

    @abc.abstractmethod
    def build_sample_request(self) -> Tuple[str, List[str], List[FilterExpr], str | None, str, bool]:
        raise NotImplementedError

    @abc.abstractmethod
    def evaluate(self, rows: List[Dict[str, Any]]) -> List[EvidenceRow]:
        raise NotImplementedError


__all__ = ["BaseScoutCheck", "PreflightResult"]
