"""Concierge apply executor for local config actions."""

from __future__ import annotations

import json
import os
import secrets
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict

from g_gremlin.autopilot.audit_log import AuditLog
from g_gremlin.autopilot.concierge_planner import ConciergeAction, ConciergePlan
from g_gremlin.autopilot.daemon import bump_runtime_policy_revision, write_runtime_reload_signal
from g_gremlin.autopilot.modules import ModuleStore, default_module_envelope
from g_gremlin.autopilot.scout_config_store import ScoutConfigStore


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _timestamp_token() -> str:
    return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")


class ConciergeActionResult(BaseModel):
    model_config = ConfigDict(extra="ignore")

    action_id: str
    action_type: str
    target: str
    status: Literal["applied", "dry_run", "skipped", "error"]
    before: dict[str, Any] | None = None
    after: dict[str, Any] | None = None
    error: str | None = None


class ConciergeReceiptStats(BaseModel):
    model_config = ConfigDict(extra="ignore")

    total: int
    applied: int
    dry_run_count: int
    skipped: int
    errors: int


class ConciergeReceipt(BaseModel):
    model_config = ConfigDict(extra="ignore")

    run_id: str
    dry_run: bool
    actor: str
    started_at: str
    completed_at: str
    outcome: Literal["success", "partial_failure", "failed", "dry_run"]
    results: list[ConciergeActionResult]
    stats: ConciergeReceiptStats


class ConciergeExecutor:
    _ACTION_ORDER: dict[str, int] = {
        "scout_config.upsert": 0,
        "module_config.upsert": 1,
        "scout_run.execute": 2,
        "runtime_policy.reload": 3,
    }

    def __init__(
        self,
        scout_config_store: ScoutConfigStore,
        module_store: ModuleStore,
        state_dir: Path,
        *,
        audit_log: AuditLog | None = None,
    ) -> None:
        self.scout_config_store = scout_config_store
        self.module_store = module_store
        self.state_dir = Path(state_dir)
        self.audit_log = audit_log

    def execute(
        self,
        plan: ConciergePlan,
        *,
        dry_run: bool = True,
        actor: str = "concierge",
    ) -> ConciergeReceipt:
        started_at = _utc_now_iso()
        results: list[ConciergeActionResult] = []

        for action in self._ordered_actions(plan.actions):
            try:
                if action.action_type == "scout_config.upsert":
                    result = self._execute_scout_config_upsert(
                        action,
                        plan_run_id=plan.run_id,
                        dry_run=dry_run,
                        actor=actor,
                    )
                elif action.action_type == "module_config.upsert":
                    result = self._execute_module_config_upsert(
                        action,
                        plan_run_id=plan.run_id,
                        dry_run=dry_run,
                        actor=actor,
                    )
                elif action.action_type == "scout_run.execute":
                    result = self._execute_scout_run(
                        action,
                        plan_run_id=plan.run_id,
                        dry_run=dry_run,
                        actor=actor,
                    )
                elif action.action_type == "runtime_policy.reload":
                    result = self._execute_policy_reload(
                        action,
                        plan_run_id=plan.run_id,
                        dry_run=dry_run,
                        actor=actor,
                    )
                else:
                    result = ConciergeActionResult(
                        action_id=action.action_id,
                        action_type=action.action_type,
                        target=action.target,
                        status="skipped",
                        error=f"Unsupported action_type: {action.action_type}",
                    )
                results.append(result)
            except Exception as exc:
                results.append(
                    ConciergeActionResult(
                        action_id=action.action_id,
                        action_type=action.action_type,
                        target=action.target,
                        status="error",
                        error=str(exc),
                    )
                )

        completed_at = _utc_now_iso()
        stats = self._compute_stats(results)
        outcome = self._compute_outcome(stats=stats, dry_run=dry_run)
        receipt = ConciergeReceipt(
            run_id=plan.run_id,
            dry_run=bool(dry_run),
            actor=actor,
            started_at=started_at,
            completed_at=completed_at,
            outcome=outcome,
            results=results,
            stats=stats,
        )
        self._persist_receipt(receipt)
        return receipt

    def _execute_scout_config_upsert(
        self,
        action: ConciergeAction,
        *,
        plan_run_id: str,
        dry_run: bool,
        actor: str,
    ) -> ConciergeActionResult:
        current = self.scout_config_store.load_scout(action.target)
        diff_before = dict(action.diff.before) if action.diff else {}
        diff_after = dict(action.diff.after) if action.diff else {}

        changed_params: dict[str, Any] = {}
        for key, value in diff_after.items():
            if diff_before.get(key) != value:
                changed_params[key] = value

        if not changed_params:
            result = ConciergeActionResult(
                action_id=action.action_id,
                action_type=action.action_type,
                target=action.target,
                status="skipped",
                before=current,
                after=current,
                error="No changed parameters in action diff",
            )
            self._log(
                event_type="concierge_apply",
                actor=actor,
                summary=f"Skipped {action.action_type} for {action.target}",
                scout_name=action.target,
                run_id=plan_run_id,
                detail={
                    "action_type": action.action_type,
                    "before": current,
                    "after": current,
                    "dry_run": dry_run,
                    "status": result.status,
                },
            )
            return result

        proposed = dict(current)
        proposed.update(changed_params)

        if dry_run:
            result = ConciergeActionResult(
                action_id=action.action_id,
                action_type=action.action_type,
                target=action.target,
                status="dry_run",
                before=current,
                after=proposed,
            )
        else:
            self.scout_config_store.save_scout(action.target, proposed, actor=actor)
            persisted = self.scout_config_store.load_scout(action.target)
            result = ConciergeActionResult(
                action_id=action.action_id,
                action_type=action.action_type,
                target=action.target,
                status="applied",
                before=current,
                after=persisted,
            )

        self._log(
            event_type="concierge_apply",
            actor=actor,
            summary=(
                f"{'Dry-run' if dry_run else 'Applied'} {action.action_type} for {action.target}"
            ),
            scout_name=action.target,
            run_id=plan_run_id,
            detail={
                "action_type": action.action_type,
                "before": current,
                "after": result.after,
                "dry_run": dry_run,
                "status": result.status,
            },
        )
        return result

    def _execute_module_config_upsert(
        self,
        action: ConciergeAction,
        *,
        plan_run_id: str,
        dry_run: bool,
        actor: str,
    ) -> ConciergeActionResult:
        try:
            envelope = self.module_store.load_module(action.target)
        except Exception:
            envelope = default_module_envelope(action.target)
        current_config = dict(envelope.config or {})

        note = "No module config change proposed; configure this module via the Modules page."
        result = ConciergeActionResult(
            action_id=action.action_id,
            action_type=action.action_type,
            target=action.target,
            status="skipped",
            before=current_config,
            after={"config": current_config, "note": note},
            error=note,
        )

        self._log(
            event_type="concierge_apply",
            actor=actor,
            summary=f"Skipped {action.action_type} for {action.target}",
            module_id=action.target,
            run_id=plan_run_id,
            detail={
                "action_type": action.action_type,
                "before": current_config,
                "after": result.after,
                "dry_run": dry_run,
                "status": result.status,
            },
        )
        return result

    def _execute_scout_run(
        self,
        action: ConciergeAction,
        *,
        plan_run_id: str,
        dry_run: bool,
        actor: str,
    ) -> ConciergeActionResult:
        after = {"signal": "validation_scan_requested"}
        result = ConciergeActionResult(
            action_id=action.action_id,
            action_type=action.action_type,
            target=action.target,
            status="dry_run" if dry_run else "applied",
            after=after,
        )
        self._log(
            event_type="concierge_apply",
            actor=actor,
            summary=f"{'Dry-run' if dry_run else 'Applied'} {action.action_type}",
            run_id=plan_run_id,
            detail={
                "action_type": action.action_type,
                "dry_run": dry_run,
                "status": result.status,
                "after": after,
            },
        )
        return result

    def _execute_policy_reload(
        self,
        action: ConciergeAction,
        *,
        plan_run_id: str,
        dry_run: bool,
        actor: str,
    ) -> ConciergeActionResult:
        if dry_run:
            result = ConciergeActionResult(
                action_id=action.action_id,
                action_type=action.action_type,
                target=action.target,
                status="dry_run",
            )
            self._log(
                event_type="concierge_apply",
                actor=actor,
                summary=f"Dry-run {action.action_type}",
                run_id=plan_run_id,
                detail={
                    "action_type": action.action_type,
                    "dry_run": True,
                    "status": result.status,
                },
            )
            return result

        revision = bump_runtime_policy_revision(self.state_dir)
        write_runtime_reload_signal(self.state_dir, revision=revision)
        result = ConciergeActionResult(
            action_id=action.action_id,
            action_type=action.action_type,
            target=action.target,
            status="applied",
            after={"revision": revision},
        )
        self._log(
            event_type="concierge_apply",
            actor=actor,
            summary=f"Applied {action.action_type} revision={revision}",
            run_id=plan_run_id,
            detail={
                "action_type": action.action_type,
                "dry_run": False,
                "status": result.status,
                "revision": revision,
            },
        )
        return result

    def _persist_receipt(self, receipt: ConciergeReceipt) -> Path:
        receipts_dir = self.state_dir / "concierge_receipts"
        receipts_dir.mkdir(parents=True, exist_ok=True)
        path = receipts_dir / f"{receipt.run_id}_{_timestamp_token()}.json"
        tmp_path = path.with_name(f"{path.name}.{secrets.token_hex(4)}.tmp")
        tmp_path.write_text(
            json.dumps(receipt.model_dump(exclude_none=False), indent=2),
            encoding="utf-8",
        )
        os.replace(tmp_path, path)
        return path

    @staticmethod
    def _ordered_actions(actions: list[ConciergeAction]) -> list[ConciergeAction]:
        indexed = list(enumerate(actions))
        indexed.sort(
            key=lambda item: (
                ConciergeExecutor._ACTION_ORDER.get(item[1].action_type, 99),
                item[0],
            )
        )
        return [item[1] for item in indexed]

    @staticmethod
    def _compute_stats(results: list[ConciergeActionResult]) -> ConciergeReceiptStats:
        return ConciergeReceiptStats(
            total=len(results),
            applied=sum(1 for result in results if result.status == "applied"),
            dry_run_count=sum(1 for result in results if result.status == "dry_run"),
            skipped=sum(1 for result in results if result.status == "skipped"),
            errors=sum(1 for result in results if result.status == "error"),
        )

    @staticmethod
    def _compute_outcome(
        *,
        stats: ConciergeReceiptStats,
        dry_run: bool,
    ) -> Literal["success", "partial_failure", "failed", "dry_run"]:
        if stats.total == 0:
            return "success"
        if stats.errors == stats.total:
            return "failed"
        if stats.errors > 0:
            return "partial_failure"
        if dry_run and stats.dry_run_count == stats.total:
            return "dry_run"
        return "success"

    def _log(
        self,
        *,
        event_type: str,
        actor: str,
        summary: str,
        integration: str | None = None,
        module_id: str | None = None,
        scout_name: str | None = None,
        detail: dict[str, Any] | None = None,
        severity: str = "info",
        run_id: str | None = None,
    ) -> None:
        if self.audit_log is None:
            return
        self.audit_log.log(
            event_type,
            actor,
            summary,
            integration=integration,
            module_id=module_id,
            scout_name=scout_name,
            detail=detail,
            severity=severity,
            run_id=run_id,
        )


__all__ = [
    "ConciergeActionResult",
    "ConciergeReceiptStats",
    "ConciergeReceipt",
    "ConciergeExecutor",
]
