"""AwoooP Ansible candidate backfill worker.

This worker closes the gap between "AI found an allowlisted PlayBook candidate"
and "the check-mode worker has a durable AOL row to claim". It does not execute
host writes by itself; it only writes ``ansible_candidate_matched`` rows for
recent unresolved incidents that already match the static Ansible catalog.
"""

from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable
from typing import Any

import structlog
from sqlalchemy import text

from src.core.config import settings
from src.db.base import get_db_context
from src.services.awooop_ansible_audit_service import (
    build_ansible_decision_audit_payload,
    record_ansible_decision_audit,
)
from src.services.awooop_ansible_check_mode_service import (
    backfill_missing_auto_repair_execution_receipts_once,
)

logger = structlog.get_logger(__name__)

Recorder = Callable[..., Awaitable[bool]]

_BACKFILL_DECISION_PATH = "repair_candidate_controlled_queue"
_BACKFILL_REASON = (
    "truth-chain found allowlisted Ansible catalog candidates but no durable "
    "candidate row existed; enqueue for check-mode worker"
)


async def _fetch_missing_candidate_incidents(
    *,
    project_id: str,
    window_hours: int,
    scan_limit: int,
) -> list[dict[str, Any]]:
    async with get_db_context(project_id) as db:
        await db.execute(text("SET LOCAL statement_timeout = '5000ms'"))
        result = await db.execute(
            text("""
                SELECT
                    incident_id,
                    project_id,
                    status::text AS status,
                    severity::text AS severity,
                    alertname,
                    alert_category,
                    notification_type,
                    created_at,
                    updated_at,
                    resolved_at,
                    verification_result,
                    frequency_snapshot,
                    signals,
                    decision_chain
                FROM incidents
                WHERE (project_id = :project_id OR project_id IS NULL)
                  AND created_at >= NOW() - (:window_hours * INTERVAL '1 hour')
                  AND resolved_at IS NULL
                  AND upper(coalesce(status::text, '')) NOT IN ('RESOLVED', 'CLOSED')
                  AND NOT EXISTS (
                      SELECT 1
                      FROM automation_operation_log existing
                      WHERE existing.operation_type = 'ansible_candidate_matched'
                        AND existing.created_at >= NOW() - (:window_hours * INTERVAL '1 hour')
                        AND existing.input ->> 'executor' = 'ansible'
                        AND coalesce(existing.incident_id::text, existing.input ->> 'incident_id') = incidents.incident_id::text
                  )
                ORDER BY created_at DESC
                LIMIT :scan_limit
            """),
            {
                "project_id": project_id,
                "window_hours": max(1, window_hours),
                "scan_limit": max(1, scan_limit),
            },
        )
        return [dict(row) for row in result.mappings().all()]


def _build_backfill_proposal(incident: dict[str, Any]) -> dict[str, Any]:
    return {
        "source": "truth_chain_candidate_backfill",
        "risk_level": str(incident.get("severity") or ""),
        "action": "enqueue_allowlisted_ansible_check_mode",
        "alertname": incident.get("alertname"),
    }


async def enqueue_missing_ansible_candidates_once(
    *,
    project_id: str = "awoooi",
    limit: int | None = None,
    window_hours: int | None = None,
    recorder: Recorder = record_ansible_decision_audit,
    receipt_backfiller: Callable[..., Awaitable[dict[str, Any]]] = backfill_missing_auto_repair_execution_receipts_once,
) -> dict[str, Any]:
    """Backfill missing Ansible candidate rows for recent unresolved incidents."""

    if not settings.ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER:
        return {
            "skipped": True,
            "scanned": 0,
            "queued": 0,
            "already_existing_or_write_skipped": 0,
            "no_catalog_candidate": 0,
            "error": None,
        }

    bounded_limit = max(1, limit or settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_BATCH_LIMIT)
    bounded_window_hours = max(
        1,
        window_hours or settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WINDOW_HOURS,
    )
    scan_limit = min(100, max(25, bounded_limit * 5))
    stats: dict[str, Any] = {
        "skipped": False,
        "scanned": 0,
        "queued": 0,
        "already_existing_or_write_skipped": 0,
        "no_catalog_candidate": 0,
        "repair_receipts_backfilled": 0,
        "error": None,
    }

    try:
        incidents = await _fetch_missing_candidate_incidents(
            project_id=project_id,
            window_hours=bounded_window_hours,
            scan_limit=scan_limit,
        )
        stats["scanned"] = len(incidents)
        for incident in incidents:
            if stats["queued"] >= bounded_limit:
                break
            payload = build_ansible_decision_audit_payload(
                incident=incident,
                proposal_data=_build_backfill_proposal(incident),
                decision_path=_BACKFILL_DECISION_PATH,
                not_used_reason=_BACKFILL_REASON,
            )
            if payload is None:
                stats["no_catalog_candidate"] += 1
                continue
            inserted = await recorder(
                incident=incident,
                proposal_data=_build_backfill_proposal(incident),
                decision_path=_BACKFILL_DECISION_PATH,
                not_used_reason=_BACKFILL_REASON,
            )
            if inserted:
                stats["queued"] += 1
            else:
                stats["already_existing_or_write_skipped"] += 1
        receipt_stats = await receipt_backfiller(
            project_id=project_id,
            window_hours=bounded_window_hours,
            limit=bounded_limit,
        )
        stats["repair_receipts_backfilled"] = int(receipt_stats.get("written") or 0)
        if receipt_stats.get("error") and not stats["error"]:
            stats["error"] = receipt_stats["error"]
    except Exception as exc:
        stats["error"] = f"{type(exc).__name__}: {exc}"[:500]
        logger.warning("awooop_ansible_candidate_backfill_once_failed", **stats)

    logger.info("awooop_ansible_candidate_backfill_once_done", **stats)
    return stats


async def run_awooop_ansible_candidate_backfill_loop() -> None:
    if not settings.ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER:
        logger.info("awooop_ansible_candidate_backfill_worker_disabled")
        return

    logger.info(
        "awooop_ansible_candidate_backfill_worker_started",
        interval_seconds=settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_INTERVAL_SECONDS,
        batch_limit=settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_BATCH_LIMIT,
        window_hours=settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WINDOW_HOURS,
    )
    await asyncio.sleep(settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_STARTUP_SLEEP_SECONDS)

    while True:
        try:
            result = await enqueue_missing_ansible_candidates_once(
                limit=settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_BATCH_LIMIT,
                window_hours=settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WINDOW_HOURS,
            )
            if result.get("queued") or result.get("error"):
                logger.info("awooop_ansible_candidate_backfill_worker_tick", **result)
        except Exception as exc:
            logger.warning("awooop_ansible_candidate_backfill_worker_failed", error=str(exc))

        await asyncio.sleep(settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_INTERVAL_SECONDS)
