"""Scout execution engine."""

from __future__ import annotations

import importlib
import json
import time
from typing import Any, Dict, Iterable, List, Tuple

from .loader import ScoutLoader
from .models import ScoutDefinition, ScoutFinding, ScoutScanResult
from .providers import create_provider


class ScoutEngine:
    def __init__(
        self,
        loader: ScoutLoader | None = None,
        *,
        user_credentials: set[str] | None = None,
    ) -> None:
        self.user_credentials = user_credentials or set()
        self.loader = loader or ScoutLoader(user_credentials=self.user_credentials)

    def list_scouts(self) -> List[ScoutDefinition]:
        return self.loader.list_scouts()

    def get_scout(self, name: str) -> ScoutDefinition | None:
        return self.loader.get_scout(name)

    def resolve_params(
        self,
        definition: ScoutDefinition,
        overrides: Dict[str, Any] | None = None,
    ) -> Dict[str, Any]:
        params: Dict[str, Any] = {}
        overrides = overrides or {}

        for param in definition.parameters:
            value = param.default
            if param.name in overrides:
                value = overrides[param.name]
            value = _coerce_param_value(param.type, value)
            if param.required and (value is None or value == ""):
                raise ValueError(f"Missing required param '{param.name}' for scout {definition.name}")
            params[param.name] = value

        # Include any unknown overrides for forward compatibility
        for key, value in overrides.items():
            if key not in params:
                params[key] = value

        return params

    def _check_credentials(self, definition: ScoutDefinition, crm: str) -> Tuple[bool, List[str]]:
        required = self._required_credentials(definition, crm)
        if not required:
            return True, []
        missing = [c for c in required if c not in self.user_credentials]
        return len(missing) == 0, missing

    def _required_credentials(self, definition: ScoutDefinition, crm: str) -> List[str]:
        required = list(definition.requires_credentials or [])
        if not required:
            return []
        crm_key = (crm or "salesforce").lower()
        known_crms = {"salesforce", "sfdc", "hubspot", "dynamics"}
        req_lower = [r.lower() for r in required]
        if any(r in known_crms for r in req_lower):
            required = [r for r in required if r.lower() not in known_crms]
            required.append(crm_key)
        return required

    def run_scout(
        self,
        name: str,
        *,
        params: Dict[str, Any] | None = None,
        top_n: int = 20,
        org_alias: str | None = None,
        crm: str = "salesforce",
        hubspot_profile: str | None = None,
        dynamics_profile: str | None = None,
    ) -> ScoutFinding:
        definition = self.loader.get_scout(name)
        if not definition:
            raise ValueError(f"Scout '{name}' not found")
        if not definition.enabled:
            return ScoutFinding(
                scout_name=definition.name,
                display_name=definition.display_name,
                description=definition.description,
                category=definition.category,
                severity=definition.severity_default,
                status="unsupported",
                status_detail="Scout disabled",
            )

        if definition.supported_crms:
            crm_key = (crm or "salesforce").lower()
            supported = [c.lower() for c in definition.supported_crms]
            if crm_key not in supported:
                return ScoutFinding(
                    scout_name=definition.name,
                    display_name=definition.display_name,
                    description=definition.description,
                    category=definition.category,
                    severity=definition.severity_default,
                    status="unsupported",
                    status_detail=f"Unsupported CRM: {crm}",
                )

        ok, missing = self._check_credentials(definition, crm)
        if not ok:
            return ScoutFinding(
                scout_name=definition.name,
                display_name=definition.display_name,
                description=definition.description,
                category=definition.category,
                severity=definition.severity_default,
                status="unsupported",
                status_detail=f"Missing credentials: {', '.join(missing)}",
            )

        resolved_params = self.resolve_params(definition, params)

        field_overrides: Dict[str, Dict[str, str]] | None = None
        if definition.field_overrides:
            field_overrides = definition.field_overrides.get(crm) or definition.field_overrides.get(
                crm.lower()
            )

        check_cls = _import_check(definition.check_module, definition.check_class)
        provider = create_provider(
            crm,
            org_alias=org_alias,
            hubspot_profile=hubspot_profile,
            dynamics_profile=dynamics_profile,
            field_overrides=field_overrides,
        )
        check = check_cls(definition, resolved_params, top_n, org_alias, provider=provider)
        finding = check.execute()

        # Ensure metadata is filled
        if not finding.display_name:
            finding.display_name = definition.display_name
        if not finding.description:
            finding.description = definition.description
        if not finding.category:
            finding.category = definition.category
        if not finding.severity:
            finding.severity = definition.severity_default
        return finding

    def run_scan(
        self,
        scout_names: Iterable[str],
        *,
        top_n: int = 20,
        per_scout_params: Dict[str, Dict[str, Any]] | None = None,
        org_alias: str | None = None,
        crm: str = "salesforce",
        hubspot_profile: str | None = None,
        dynamics_profile: str | None = None,
    ) -> ScoutScanResult:
        start_time = time.perf_counter()
        started_at = _utc_now_iso()
        per_scout_params = per_scout_params or {}
        findings: List[ScoutFinding] = []
        errors: List[str] = []

        for name in scout_names:
            try:
                params = per_scout_params.get(name, {})
                finding = self.run_scout(
                    name,
                    params=params,
                    top_n=top_n,
                    org_alias=org_alias,
                    crm=crm,
                    hubspot_profile=hubspot_profile,
                    dynamics_profile=dynamics_profile,
                )
                findings.append(finding)
            except Exception as exc:
                errors.append(f"{name}: {exc}")

        ended_at = time.perf_counter()
        result = ScoutScanResult(
            findings=findings,
            errors=errors,
            total_duration_ms=int((ended_at - start_time) * 1000),
            started_at=started_at,
            org_alias=org_alias,
        )
        result.ended_at = _utc_now_iso()
        return result


def _import_check(module_path: str, class_name: str):
    if not module_path or not class_name:
        raise ValueError("Scout definition missing check_module/check_class")
    module = importlib.import_module(module_path)
    if not hasattr(module, class_name):
        raise ValueError(f"Check class '{class_name}' not found in {module_path}")
    return getattr(module, class_name)


def _coerce_param_value(param_type: str, value: Any) -> Any:
    if value is None:
        return None

    if param_type == "boolean":
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            lowered = value.lower()
            if lowered in ("true", "yes", "1"):
                return True
            if lowered in ("false", "no", "0"):
                return False
        return bool(value)

    if param_type == "number":
        if isinstance(value, (int, float)):
            return value
        if isinstance(value, str):
            try:
                return float(value) if "." in value else int(value)
            except Exception:
                raise ValueError(f"Invalid number parameter: {value}")
        return value

    if param_type == "list":
        if isinstance(value, list):
            return value
        if isinstance(value, str):
            text = value.strip()
            if text.startswith("["):
                try:
                    parsed = json.loads(text)
                    return parsed if isinstance(parsed, list) else [parsed]
                except Exception:
                    return [v.strip() for v in text.split(",") if v.strip()]
            return [v.strip() for v in text.split(",") if v.strip()]
        return [value]

    if param_type == "object":
        if isinstance(value, dict):
            return value
        if isinstance(value, str):
            text = value.strip()
            if text.startswith("{"):
                try:
                    parsed = json.loads(text)
                    return parsed if isinstance(parsed, dict) else value
                except Exception:
                    return value
        return value

    # string or unknown
    return str(value) if value is not None else value


def _utc_now_iso() -> str:
    from datetime import datetime, timezone

    return datetime.now(timezone.utc).isoformat()

__all__ = ["ScoutEngine"]
