"""
NeMo/Nemotron External Runner Preflight
======================================

Validates the local request pack before it is handed to an approved external
NeMo/NIM/Nemotron runner. This module does not call external services, tools,
production systems, or LLMs.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import Any

from src.services.agent_nemotron_replay_adapter import (
    NEMOTRON_CANDIDATE_ID,
    REQUEST_SCHEMA_VERSION,
)
from src.services.agent_replay_input import assert_no_evaluation_label_leak

PREFLIGHT_SCHEMA_VERSION = "agent_nemotron_external_runner_preflight_v1"

_REQUIRED_RESPONSE_FIELDS = {
    "proposed_action",
    "action_plan",
    "risk_level",
    "requires_human_approval",
    "blocked_by_policy",
}
_FORBIDDEN_TEXT_MARKERS = {
    "evaluation_labels",
    "verification_result",
    "execution_success",
    "execution_error",
    "self_healing_score",
    "rca_correct",
    "tool_dry_run_pass",
    "repair_success",
    "false_repair",
}
_SENSITIVE_TEXT_MARKERS = {
    "authorization",
    "bearer ",
    "basic ",
    "password",
    "passwd",
    "api_key",
    "secret",
    "token",
}


@dataclass(frozen=True)
class NemotronExternalRunnerPreflightReport:
    """Preflight decision for a NeMo external replay request pack."""

    fixtures: int
    candidate_inputs: int
    requests: int
    valid: bool
    failures: list[str] = field(default_factory=list)
    duplicate_fixtures: list[str] = field(default_factory=list)
    duplicate_candidate_inputs: list[str] = field(default_factory=list)
    duplicate_requests: list[str] = field(default_factory=list)
    missing_candidate_inputs: list[str] = field(default_factory=list)
    missing_requests: list[str] = field(default_factory=list)
    unexpected_candidate_inputs: list[str] = field(default_factory=list)
    unexpected_requests: list[str] = field(default_factory=list)
    candidate_input_label_leak_records: int = 0
    request_context_label_leak_records: int = 0
    request_only_records: int = 0
    not_replacement_evidence_records: int = 0
    expected_action_marker_records: int = 0
    sensitive_marker_present_in_context: bool = False
    sensitive_marker_records: int = 0
    sensitive_marker_distribution: dict[str, int] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "schema_version": PREFLIGHT_SCHEMA_VERSION,
            "candidate_id": NEMOTRON_CANDIDATE_ID,
            "fixtures": self.fixtures,
            "candidate_inputs": self.candidate_inputs,
            "requests": self.requests,
            "valid": self.valid,
            "failures": list(self.failures),
            "duplicate_fixtures": list(self.duplicate_fixtures),
            "duplicate_candidate_inputs": list(self.duplicate_candidate_inputs),
            "duplicate_requests": list(self.duplicate_requests),
            "missing_candidate_inputs": list(self.missing_candidate_inputs),
            "missing_requests": list(self.missing_requests),
            "unexpected_candidate_inputs": list(self.unexpected_candidate_inputs),
            "unexpected_requests": list(self.unexpected_requests),
            "candidate_input_label_leak_records": self.candidate_input_label_leak_records,
            "request_context_label_leak_records": self.request_context_label_leak_records,
            "request_only_records": self.request_only_records,
            "not_replacement_evidence_records": self.not_replacement_evidence_records,
            "expected_action_marker_records": self.expected_action_marker_records,
            "sensitive_marker_present_in_context": self.sensitive_marker_present_in_context,
            "sensitive_marker_records": self.sensitive_marker_records,
            "sensitive_marker_distribution": dict(self.sensitive_marker_distribution),
        }


def evaluate_nemotron_external_runner_preflight(
    *,
    fixtures: list[dict[str, Any]],
    candidate_inputs: list[dict[str, Any]],
    requests: list[dict[str, Any]],
) -> NemotronExternalRunnerPreflightReport:
    """Validate request-pack readiness before an external NeMo runner consumes it."""
    failures: list[str] = []
    fixture_index, duplicate_fixtures = _index_records(fixtures, "fixture", failures)
    input_index, duplicate_inputs = _index_records(
        candidate_inputs,
        "candidate_input",
        failures,
    )
    request_index, duplicate_requests = _index_records(requests, "request", failures)

    fixture_keys = set(fixture_index)
    input_keys = set(input_index)
    request_keys = set(request_index)

    missing_inputs = sorted(_render_key(key) for key in fixture_keys - input_keys)
    unexpected_inputs = sorted(_render_key(key) for key in input_keys - fixture_keys)
    missing_requests = sorted(_render_key(key) for key in input_keys - request_keys)
    unexpected_requests = sorted(_render_key(key) for key in request_keys - input_keys)

    if missing_inputs:
        failures.append(f"missing_candidate_inputs:{','.join(missing_inputs)}")
    if unexpected_inputs:
        failures.append(
            f"unexpected_candidate_inputs:{','.join(unexpected_inputs)}"
        )
    if missing_requests:
        failures.append(f"missing_requests:{','.join(missing_requests)}")
    if unexpected_requests:
        failures.append(f"unexpected_requests:{','.join(unexpected_requests)}")

    candidate_input_label_leak_records = _candidate_input_label_leaks(
        candidate_inputs,
        failures,
    )
    request_context_label_leak_records = _request_context_label_leaks(
        requests,
        failures,
    )
    request_only_records = _count_request_metadata(requests, "request_only", True)
    not_replacement_evidence_records = _count_request_metadata(
        requests,
        "not_replacement_evidence",
        True,
    )
    expected_action_marker_records = sum(
        1
        for fixture in fixtures
        if _expected_action_markers(fixture)
    )
    sensitive_marker_records, sensitive_marker_distribution = _sensitive_marker_scan(
        candidate_inputs,
        requests,
    )
    sensitive_marker_present = sensitive_marker_records > 0
    if sensitive_marker_present:
        failures.append(f"sensitive_marker_present_in_context:{sensitive_marker_records}")

    _validate_requests(requests, failures)
    _validate_context_alignment(
        fixture_index=fixture_index,
        input_index=input_index,
        request_index=request_index,
        failures=failures,
    )

    return NemotronExternalRunnerPreflightReport(
        fixtures=len(fixtures),
        candidate_inputs=len(candidate_inputs),
        requests=len(requests),
        valid=not failures,
        failures=failures,
        duplicate_fixtures=duplicate_fixtures,
        duplicate_candidate_inputs=duplicate_inputs,
        duplicate_requests=duplicate_requests,
        missing_candidate_inputs=missing_inputs,
        missing_requests=missing_requests,
        unexpected_candidate_inputs=unexpected_inputs,
        unexpected_requests=unexpected_requests,
        candidate_input_label_leak_records=candidate_input_label_leak_records,
        request_context_label_leak_records=request_context_label_leak_records,
        request_only_records=request_only_records,
        not_replacement_evidence_records=not_replacement_evidence_records,
        expected_action_marker_records=expected_action_marker_records,
        sensitive_marker_present_in_context=sensitive_marker_present,
        sensitive_marker_records=sensitive_marker_records,
        sensitive_marker_distribution=sensitive_marker_distribution,
    )


def _index_records(
    records: list[dict[str, Any]],
    name: str,
    failures: list[str],
) -> tuple[dict[tuple[str, str], dict[str, Any]], list[str]]:
    indexed: dict[tuple[str, str], dict[str, Any]] = {}
    duplicates: list[str] = []
    for line_number, record in enumerate(records, start=1):
        key = _run_incident_key(record)
        if key is None:
            failures.append(f"invalid_{name}:line_{line_number}:missing_run_or_incident")
            continue
        if key in indexed:
            rendered = _render_key(key)
            duplicates.append(rendered)
            failures.append(f"duplicate_{name}:line_{line_number}:{rendered}")
            continue
        indexed[key] = record
    return indexed, sorted(set(duplicates))


def _candidate_input_label_leaks(
    candidate_inputs: list[dict[str, Any]],
    failures: list[str],
) -> int:
    leaks = 0
    for line_number, candidate_input in enumerate(candidate_inputs, start=1):
        try:
            assert_no_evaluation_label_leak(candidate_input)
        except Exception as exc:
            leaks += 1
            failures.append(f"candidate_input_label_leak:line_{line_number}:{exc}")
    return leaks


def _request_context_label_leaks(
    requests: list[dict[str, Any]],
    failures: list[str],
) -> int:
    leaks = 0
    for line_number, request in enumerate(requests, start=1):
        visible_payload = {
            "incident_context": request.get("incident_context") or {},
            "source_metadata": request.get("source_metadata") or {},
            "user_prompt": request.get("user_prompt") or "",
        }
        markers = _forbidden_text_markers(visible_payload)
        if markers:
            leaks += 1
            failures.append(
                f"request_context_label_leak:line_{line_number}:"
                f"{','.join(markers)}"
            )
    return leaks


def _validate_requests(
    requests: list[dict[str, Any]],
    failures: list[str],
) -> None:
    for line_number, request in enumerate(requests, start=1):
        if request.get("schema_version") != REQUEST_SCHEMA_VERSION:
            failures.append(f"request_schema_mismatch:line_{line_number}")
        if request.get("candidate_id") != NEMOTRON_CANDIDATE_ID:
            failures.append(f"request_candidate_mismatch:line_{line_number}")
        metadata = dict(request.get("metadata") or {})
        if metadata.get("request_only") is not True:
            failures.append(f"request_not_request_only:line_{line_number}")
        if metadata.get("not_replacement_evidence") is not True:
            failures.append(f"request_missing_not_replacement_evidence:line_{line_number}")
        required = set((request.get("response_contract") or {}).get("required") or [])
        missing_response_fields = sorted(_REQUIRED_RESPONSE_FIELDS - required)
        if missing_response_fields:
            failures.append(
                "request_response_contract_missing:"
                f"line_{line_number}:{','.join(missing_response_fields)}"
            )


def _validate_context_alignment(
    *,
    fixture_index: dict[tuple[str, str], dict[str, Any]],
    input_index: dict[tuple[str, str], dict[str, Any]],
    request_index: dict[tuple[str, str], dict[str, Any]],
    failures: list[str],
) -> None:
    for key in sorted(set(fixture_index) & set(input_index)):
        if fixture_index[key].get("incident_context") != input_index[key].get(
            "incident_context"
        ):
            failures.append(f"fixture_input_context_mismatch:{_render_key(key)}")

    for key in sorted(set(input_index) & set(request_index)):
        candidate_input = input_index[key]
        request = request_index[key]
        if candidate_input.get("incident_context") != request.get("incident_context"):
            failures.append(f"input_request_context_mismatch:{_render_key(key)}")
        if candidate_input.get("source_metadata") != request.get("source_metadata"):
            failures.append(f"input_request_metadata_mismatch:{_render_key(key)}")


def _count_request_metadata(
    requests: list[dict[str, Any]],
    key: str,
    expected: Any,
) -> int:
    return sum(
        1
        for request in requests
        if (request.get("metadata") or {}).get(key) is expected
    )


def _expected_action_markers(fixture: dict[str, Any]) -> list[str]:
    labels = dict(fixture.get("evaluation_labels") or {})
    markers = labels.get("expected_action_markers") or []
    return [str(marker) for marker in markers if str(marker).strip()]


def _sensitive_marker_scan(
    candidate_inputs: list[dict[str, Any]],
    requests: list[dict[str, Any]],
) -> tuple[int, dict[str, int]]:
    distribution = dict.fromkeys(sorted(_SENSITIVE_TEXT_MARKERS), 0)
    hit_records: set[tuple[str, str]] = set()
    for record in [*candidate_inputs, *requests]:
        key = _run_incident_key(record)
        serialized = json.dumps(
            record.get("incident_context") or {},
            ensure_ascii=False,
            sort_keys=True,
        ).lower()
        markers = [
            marker for marker in sorted(_SENSITIVE_TEXT_MARKERS) if marker in serialized
        ]
        if markers and key is not None:
            hit_records.add(key)
        for marker in markers:
            distribution[marker] += 1
    return len(hit_records), {key: value for key, value in distribution.items() if value}


def _forbidden_text_markers(payload: dict[str, Any]) -> list[str]:
    serialized = json.dumps(payload, ensure_ascii=False, sort_keys=True).lower()
    return sorted(
        marker for marker in _FORBIDDEN_TEXT_MARKERS if marker in serialized
    )


def _run_incident_key(record: dict[str, Any]) -> tuple[str, str] | None:
    run_id = str(record.get("run_id", "")).strip()
    incident_id = str(record.get("incident_id", "")).strip()
    if not run_id or not incident_id:
        return None
    return (run_id, incident_id)


def _render_key(key: tuple[str, str]) -> str:
    return f"{key[0]}::{key[1]}"
