# -*- coding: utf-8 -*-
"""
TEITAN SELF-REPAIR - INTERNAL FUNCTIONAL ASSESSOR (SAFETY-FIXED)
Alpha implementation of generic functional defect detection for internal
self-repair components with hardened Windows process isolation.

Key Security & Containment Invariants:
1. Job Object is created and fully configured BEFORE launching any child process.
2. If Job Object creation or configuration fails, returns BLOCKED immediately.
3. Child process is created in SUSPENDED state (CREATE_SUSPENDED=0x00000004).
4. AssignProcessToJobObject is explicitly executed and checked BEFORE resuming.
5. If assignment fails, the suspended process is terminated, handles closed, and BLOCKED returned.
6. Main thread / process is resumed ONLY after verified assignment using ntdll.NtResumeProcess.
7. If resumption fails, process is terminated, handles closed, and BLOCKED returned.
8. Exact WinAPI signatures with 64-bit wintypes.HANDLE and restypes configured.
9. Guaranteed cleanup of job handles, process handles, and temporary files in all paths (finally block).
10. Dynamic executor remains BLOCKED unless process containment is provably confirmed.
"""
from __future__ import annotations

import ast
import ctypes
from ctypes import wintypes
import hashlib
import json
import logging
import os
import subprocess
import sys
import tempfile
import time
import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple

logger = logging.getLogger("teitan.self_repair.functional_assessor")


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


def _compute_sha256(path: str) -> Optional[str]:
    try:
        if not os.path.isfile(path):
            return None
        with open(path, "rb") as f:
            return hashlib.sha256(f.read()).hexdigest().upper()
    except Exception:
        return None


# ==============================================================================
# DATA STRUCTURES
# ==============================================================================

class FunctionalAssessmentStatus:
    PASS = "PASS"
    FAIL = "FAIL"
    BLOCKED = "BLOCKED"
    UNPROVEN = "UNPROVEN"


@dataclass
class FunctionalContractDefinition:
    """Specification of an internal behavioral contract."""
    contract_id: str
    component_name: str
    target_file_rel: str
    description: str
    contract_runner_code: str
    timeout_seconds: float = 5.0
    allowed_categories: List[str] = field(default_factory=lambda: ["SELF_REPAIR"])


@dataclass
class FunctionalEvaluationResult:
    """Outcome of an isolated functional evaluation."""
    contract_id: str
    component_name: str
    target_file_rel: str
    file_sha256: Optional[str]
    status: str  # PASS, FAIL, BLOCKED, UNPROVEN
    duration_ms: float
    assertion_error: Optional[str] = None
    stack_trace: Optional[str] = None
    stdout: Optional[str] = None
    stderr: Optional[str] = None
    inconclusive_reason: Optional[str] = None
    timestamp: str = field(default_factory=_utc_iso)

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


# ==============================================================================
# AUTHORIZED CONTRACT CATALOG (SPECIFICATIONS ONLY, NO REPAIR CODE)
# ==============================================================================

_AUTHORIZED_INTERNAL_CONTRACTS: List[FunctionalContractDefinition] = [
    # 1. CircuitTracer Contract: Downstream nodes marked NOT_REACHED after break
    FunctionalContractDefinition(
        contract_id="contract_circuit_tracer_downstream_break",
        component_name="CircuitTracer",
        target_file_rel="app/remediation/self_repair/circuit.py",
        description="Verifies CircuitTracer marks downstream nodes NOT_REACHED following an upstream break",
        contract_runner_code="""
from app.remediation.self_repair.circuit import CircuitTracer
from app.remediation.self_repair.schemas import (
    CircuitStatus,
    EvidenceBundle,
    IncidentFailureType,
    IncidentRecord,
)

incident = IncidentRecord(
    mission_id="m_contract_circuit_eval",
    failure_type=IncidentFailureType.EXCEPTION,
    observed_symptom="Executor stage failed",
    source_component="executor",
)
evidence = EvidenceBundle(
    incident_id=incident.incident_id,
    action_history=[{"stage": "planner", "status": "SUCCESS"}],
)
tracer = CircuitTracer()
trace = tracer.trace(incident=incident, evidence=evidence)
node_map = {n.component: n.status for n in trace.nodes}

assert trace.first_proven_break == "executor", f"Expected executor break, got {trace.first_proven_break}"
assert node_map["planner"] == CircuitStatus.PASS, f"planner expected PASS, got {node_map.get('planner')}"
assert node_map["executor"] == CircuitStatus.FAIL, f"executor expected FAIL, got {node_map.get('executor')}"
assert node_map["effector"] == CircuitStatus.NOT_REACHED, f"effector expected NOT_REACHED, got {node_map.get('effector')}"
assert node_map["verification"] == CircuitStatus.NOT_REACHED, f"verification expected NOT_REACHED, got {node_map.get('verification')}"
assert "effector" in trace.downstream_not_reached, "effector must be listed in downstream_not_reached"
""",
    ),
    # 2. EvidenceComparator Contract: Correct accounting of regression new_failures
    FunctionalContractDefinition(
        contract_id="contract_evidence_comparator_regression_accounting",
        component_name="EvidenceComparator",
        target_file_rel="app/remediation/self_repair/evidence_comparator.py",
        description="Verifies EvidenceComparator counts regression failures as new_failures and candidate FAIL",
        contract_runner_code="""
from app.remediation.self_repair.evidence_comparator import EvidenceComparator
from app.remediation.self_repair.schemas import (
    EvidenceBundle,
    IncidentFailureType,
    IncidentRecord,
    VerificationResult,
)

incident = IncidentRecord(
    mission_id="m_contract_comparator_eval",
    failure_type=IncidentFailureType.FAILED_TEST,
    observed_symptom="Primary test failed",
    source_component="verification",
)
evidence = EvidenceBundle(incident_id=incident.incident_id)
verif = VerificationResult(
    candidate_id="cand_test_01",
    incident_id=incident.incident_id,
    repair_id="rep_test_01",
    compile_pass=True,
    focused_tests_pass=True,
    regression_tests_pass=False,
    regression_tests_summary={"error": "1 regression test failed"},
)
comparator = EvidenceComparator()
res = comparator.compare(incident=incident, evidence=evidence, verification=verif)
assert res.new_failures == 1, f"Expected new_failures=1, got {res.new_failures}"
assert res.candidate_result == "FAIL", f"Expected candidate_result FAIL, got {res.candidate_result}"
""",
    ),
    # 3. EvidenceCollector Contract: Source excerpt schema validation
    FunctionalContractDefinition(
        contract_id="contract_evidence_collector_source_excerpt",
        component_name="EvidenceCollector",
        target_file_rel="app/remediation/self_repair/collector.py",
        description="Verifies EvidenceCollector extracts source excerpts with required schema keys and sha256",
        contract_runner_code="""
import tempfile, os
from app.remediation.self_repair.collector import EvidenceCollector
from app.remediation.self_repair.schemas import IncidentFailureType, IncidentRecord

with tempfile.TemporaryDirectory() as td:
    sample_file = os.path.join(td, "dummy.py")
    with open(sample_file, "w", encoding="utf-8") as f:
        f.write("def dummy(): pass\\n")

    incident = IncidentRecord(
        mission_id="m_contract_collector_eval",
        failure_type=IncidentFailureType.FAILED_VERIFICATION,
        observed_symptom="Collector contract check",
        source_component="verification",
    )
    collector = EvidenceCollector()
    bundle = collector.collect(
        incident=incident,
        source_files=[sample_file],
        workspace_root=td,
    )
    assert len(bundle.source_excerpts) == 1, f"Expected 1 excerpt, got {len(bundle.source_excerpts)}"
    excerpt = bundle.source_excerpts[0]
    required = {"file_path", "line_count", "head_excerpt", "tail_excerpt", "sha256"}
    missing = required - set(excerpt.keys())
    assert not missing, f"Missing excerpt keys: {missing}"
    assert len(excerpt["sha256"]) == 64, f"Invalid SHA256 length: {excerpt['sha256']}"
""",
    ),
]


# ==============================================================================
# WINDOWS KERNEL JOB OBJECT STRUCTURES & WINAPI BINDINGS (64-BIT SAFE)
# ==============================================================================

class _IO_COUNTERS(ctypes.Structure):
    _fields_ = [
        ("ReadOperationCount", ctypes.c_ulonglong),
        ("WriteOperationCount", ctypes.c_ulonglong),
        ("OtherOperationCount", ctypes.c_ulonglong),
        ("ReadTransferCount", ctypes.c_ulonglong),
        ("WriteTransferCount", ctypes.c_ulonglong),
        ("OtherTransferCount", ctypes.c_ulonglong),
    ]


class _JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
    _fields_ = [
        ("PerProcessUserTimeLimit", ctypes.c_longlong),
        ("PerJobUserTimeLimit", ctypes.c_longlong),
        ("LimitFlags", wintypes.DWORD),
        ("MinimumWorkingSetSize", ctypes.c_size_t),
        ("MaximumWorkingSetSize", ctypes.c_size_t),
        ("ActiveProcessLimit", wintypes.DWORD),
        ("Affinity", ctypes.c_size_t),
        ("PriorityClass", wintypes.DWORD),
        ("SchedulingClass", wintypes.DWORD),
    ]


class _JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
    _fields_ = [
        ("BasicLimitInformation", _JOBOBJECT_BASIC_LIMIT_INFORMATION),
        ("IoInfo", _IO_COUNTERS),
        ("ProcessMemoryLimit", ctypes.c_size_t),
        ("JobMemoryLimit", ctypes.c_size_t),
        ("PeakProcessMemoryUsed", ctypes.c_size_t),
        ("PeakJobMemoryUsed", ctypes.c_size_t),
    ]


class _JOBOBJECT_BASIC_UI_RESTRICTIONS(ctypes.Structure):
    _fields_ = [
        ("UIRestrictionsClass", wintypes.DWORD),
    ]


# WinAPI Constants
CREATE_SUSPENDED = 0x00000004
JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008
JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100
JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x00000400
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
JobObjectBasicUIRestrictions = 4
JobObjectExtendedLimitInformation = 9
UI_RESTRICTIONS_ALL = 0x000000FF


def _configure_winapi_bindings() -> Tuple[Any, Any]:
    """Configures strict 64-bit WinAPI signatures for kernel32 and ntdll."""
    if os.name != "nt":
        return None, None

    kernel32 = ctypes.windll.kernel32
    kernel32.CreateJobObjectW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR]
    kernel32.CreateJobObjectW.restype = wintypes.HANDLE

    kernel32.SetInformationJobObject.argtypes = [
        wintypes.HANDLE,
        ctypes.c_int,
        ctypes.c_void_p,
        wintypes.DWORD,
    ]
    kernel32.SetInformationJobObject.restype = wintypes.BOOL

    kernel32.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]
    kernel32.AssignProcessToJobObject.restype = wintypes.BOOL

    kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
    kernel32.CloseHandle.restype = wintypes.BOOL

    ntdll = ctypes.windll.ntdll
    ntdll.NtResumeProcess.argtypes = [wintypes.HANDLE]
    ntdll.NtResumeProcess.restype = ctypes.c_long

    return kernel32, ntdll


def _create_configured_job_object(kernel32: Any, job_name: str, memory_mb: int = 256) -> Optional[int]:
    """
    Creates and configures a Windows Kernel Job Object enforcing:
    - ProcessMemoryLimit & JobMemoryLimit
    - KILL_ON_JOB_CLOSE & DIE_ON_UNHANDLED_EXCEPTION
    - ActiveProcessLimit = 2 (for interpreter / launcher stubs)
    - Full UI restrictions
    Returns integer HANDLE on success, or None on failure.
    """
    if not kernel32:
        return None

    h_job = kernel32.CreateJobObjectW(None, job_name)
    if not h_job:
        return None

    ext_info = _JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
    mem_bytes = memory_mb * 1024 * 1024
    ext_info.ProcessMemoryLimit = mem_bytes
    ext_info.JobMemoryLimit = mem_bytes
    ext_info.BasicLimitInformation.ActiveProcessLimit = 2
    ext_info.BasicLimitInformation.LimitFlags = (
        JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
        | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
        | JOB_OBJECT_LIMIT_ACTIVE_PROCESS
        | JOB_OBJECT_LIMIT_PROCESS_MEMORY
        | JOB_OBJECT_LIMIT_JOB_MEMORY
    )

    res_ext = kernel32.SetInformationJobObject(
        wintypes.HANDLE(h_job),
        JobObjectExtendedLimitInformation,
        ctypes.byref(ext_info),
        ctypes.sizeof(ext_info),
    )
    if not res_ext:
        kernel32.CloseHandle(wintypes.HANDLE(h_job))
        return None

    ui_info = _JOBOBJECT_BASIC_UI_RESTRICTIONS()
    ui_info.UIRestrictionsClass = UI_RESTRICTIONS_ALL
    res_ui = kernel32.SetInformationJobObject(
        wintypes.HANDLE(h_job),
        JobObjectBasicUIRestrictions,
        ctypes.byref(ui_info),
        ctypes.sizeof(ui_info),
    )
    if not res_ui:
        kernel32.CloseHandle(wintypes.HANDLE(h_job))
        return None

    return int(h_job)


# ==============================================================================
# INTERNAL FUNCTIONAL ASSESSOR
# ==============================================================================

class InternalFunctionalAssessor:
    """
    Independent functional defect evaluator.
    Discovers registered contracts and tests component behavior in isolated processes.
    Enforces strict pre-execution containment: Job Object created -> Process launched
    SUSPENDED -> Assigned to Job -> Verified -> Resumed.
    """

    def __init__(
        self,
        workspace_root: str,
        python_executable: Optional[str] = None,
        contracts: Optional[List[FunctionalContractDefinition]] = None,
    ):
        self.workspace_root = os.path.realpath(workspace_root)
        self.python_exe = python_executable or sys.executable
        self.contracts = list(contracts or _AUTHORIZED_INTERNAL_CONTRACTS)
        self._kernel32, self._ntdll = _configure_winapi_bindings()

    def assess_all_components(self) -> List[FunctionalEvaluationResult]:
        """
        Executes all registered contracts without knowing any prior defect status.
        """
        results: List[FunctionalEvaluationResult] = []
        for contract in self.contracts:
            res = self.evaluate_contract(contract)
            results.append(res)
        return results

    def evaluate_contract(self, contract: FunctionalContractDefinition) -> FunctionalEvaluationResult:
        """
        Evaluates a single functional contract within a strictly isolated, bounded subprocess.
        """
        target_abs = os.path.join(self.workspace_root, contract.target_file_rel)
        t_start = time.perf_counter()

        # Step 1: Pre-flight file presence check
        if not os.path.exists(target_abs):
            duration_ms = (time.perf_counter() - t_start) * 1000.0
            return FunctionalEvaluationResult(
                contract_id=contract.contract_id,
                component_name=contract.component_name,
                target_file_rel=contract.target_file_rel,
                file_sha256=None,
                status=FunctionalAssessmentStatus.BLOCKED,
                duration_ms=duration_ms,
                inconclusive_reason=f"Target file missing at path: {target_abs}",
            )

        file_sha256 = _compute_sha256(target_abs)

        # Step 2: Syntax / AST pre-execution gate
        try:
            with open(target_abs, "r", encoding="utf-8", errors="replace") as f:
                src = f.read()
            ast.parse(src, filename=target_abs)
        except SyntaxError as syn_err:
            duration_ms = (time.perf_counter() - t_start) * 1000.0
            return FunctionalEvaluationResult(
                contract_id=contract.contract_id,
                component_name=contract.component_name,
                target_file_rel=contract.target_file_rel,
                file_sha256=file_sha256,
                status=FunctionalAssessmentStatus.BLOCKED,
                duration_ms=duration_ms,
                assertion_error=f"SyntaxError in target file: {syn_err}",
                inconclusive_reason="Syntax error prevents functional evaluation",
            )

        # Step 3: Job Object Pre-Creation & Configuration (BEFORE child process creation)
        job_handle = None
        if os.name == "nt":
            if not self._kernel32 or not self._ntdll:
                duration_ms = (time.perf_counter() - t_start) * 1000.0
                return FunctionalEvaluationResult(
                    contract_id=contract.contract_id,
                    component_name=contract.component_name,
                    target_file_rel=contract.target_file_rel,
                    file_sha256=file_sha256,
                    status=FunctionalAssessmentStatus.BLOCKED,
                    duration_ms=duration_ms,
                    inconclusive_reason="WinAPI kernel32/ntdll bindings unavailable for process containment",
                )

            job_name = f"Local\\TeitanAssessorJob_{uuid.uuid4().hex[:12]}"
            job_handle = _create_configured_job_object(self._kernel32, job_name=job_name, memory_mb=256)
            if not job_handle:
                duration_ms = (time.perf_counter() - t_start) * 1000.0
                return FunctionalEvaluationResult(
                    contract_id=contract.contract_id,
                    component_name=contract.component_name,
                    target_file_rel=contract.target_file_rel,
                    file_sha256=file_sha256,
                    status=FunctionalAssessmentStatus.BLOCKED,
                    duration_ms=duration_ms,
                    inconclusive_reason="Failed to create and configure Windows Kernel Job Object",
                )

        # Step 4: Prepare isolated test harness in temporary file
        harness_code = f"""
import sys, os
ws = r"{self.workspace_root}"
if ws in sys.path:
    sys.path.remove(ws)
sys.path.insert(0, ws)

{contract.contract_runner_code}

print("__CONTRACT_PASS__")
"""
        with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as tf:
            tf.write(harness_code)
            tf_path = tf.name

        proc = None
        try:
            env = os.environ.copy()
            env["PYTHONPATH"] = self.workspace_root
            env["PYTHONNOUSERSITE"] = "1"
            # Purge external credentials and payment keys
            for sensitive_key in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "TEITAN_SECRET"]:
                env.pop(sensitive_key, None)

            # Step 5: Create child process SUSPENDED
            creation_flags = CREATE_SUSPENDED if os.name == "nt" else 0
            proc = subprocess.Popen(
                [self.python_exe, tf_path],
                cwd=self.workspace_root,
                env=env,
                creationflags=creation_flags,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )

            # Step 6: Assign process to Job Object BEFORE resuming
            if os.name == "nt" and job_handle:
                raw_handle = int(proc._handle)
                assigned_ok = bool(
                    self._kernel32.AssignProcessToJobObject(
                        wintypes.HANDLE(job_handle),
                        wintypes.HANDLE(raw_handle),
                    )
                )
                if not assigned_ok:
                    proc.kill()
                    proc.communicate()
                    duration_ms = (time.perf_counter() - t_start) * 1000.0
                    return FunctionalEvaluationResult(
                        contract_id=contract.contract_id,
                        component_name=contract.component_name,
                        target_file_rel=contract.target_file_rel,
                        file_sha256=file_sha256,
                        status=FunctionalAssessmentStatus.BLOCKED,
                        duration_ms=duration_ms,
                        inconclusive_reason="AssignProcessToJobObject failed before process execution",
                    )

                # Step 7: Resume process ONLY after confirmed Job assignment
                nt_status = self._ntdll.NtResumeProcess(wintypes.HANDLE(raw_handle))
                if nt_status != 0:
                    proc.kill()
                    proc.communicate()
                    duration_ms = (time.perf_counter() - t_start) * 1000.0
                    return FunctionalEvaluationResult(
                        contract_id=contract.contract_id,
                        component_name=contract.component_name,
                        target_file_rel=contract.target_file_rel,
                        file_sha256=file_sha256,
                        status=FunctionalAssessmentStatus.BLOCKED,
                        duration_ms=duration_ms,
                        inconclusive_reason=f"NtResumeProcess failed with NTSTATUS: {nt_status}",
                    )

            # Step 8: Communicate with timeout containment
            stdout_data, stderr_data = proc.communicate(timeout=contract.timeout_seconds)
            duration_ms = (time.perf_counter() - t_start) * 1000.0

            if proc.returncode == 0 and "__CONTRACT_PASS__" in stdout_data:
                return FunctionalEvaluationResult(
                    contract_id=contract.contract_id,
                    component_name=contract.component_name,
                    target_file_rel=contract.target_file_rel,
                    file_sha256=file_sha256,
                    status=FunctionalAssessmentStatus.PASS,
                    duration_ms=duration_ms,
                    stdout=stdout_data.strip(),
                )

            # Extract failure line
            err_line = None
            for line in stderr_data.splitlines():
                if "AssertionError" in line:
                    err_line = line.strip()
                    break
            if not err_line:
                for line in reversed(stderr_data.splitlines()):
                    if line.strip():
                        err_line = line.strip()
                        break

            # Distinguish BEHAVIORAL FAIL from IMPORT/ENV BLOCKED
            is_import_or_env_error = any(
                k in stderr_data for k in ["ModuleNotFoundError", "ImportError", "FileNotFoundError"]
            )
            status = FunctionalAssessmentStatus.BLOCKED if is_import_or_env_error else FunctionalAssessmentStatus.FAIL

            return FunctionalEvaluationResult(
                contract_id=contract.contract_id,
                component_name=contract.component_name,
                target_file_rel=contract.target_file_rel,
                file_sha256=file_sha256,
                status=status,
                duration_ms=duration_ms,
                assertion_error=err_line or "Assertion failed without specific message",
                stack_trace=stderr_data.strip(),
                stdout=stdout_data.strip(),
                stderr=stderr_data.strip(),
                inconclusive_reason="Dependency or environment resolution failed" if is_import_or_env_error else None,
            )

        except subprocess.TimeoutExpired:
            duration_ms = (time.perf_counter() - t_start) * 1000.0
            if proc:
                proc.kill()
                proc.communicate()
            return FunctionalEvaluationResult(
                contract_id=contract.contract_id,
                component_name=contract.component_name,
                target_file_rel=contract.target_file_rel,
                file_sha256=file_sha256,
                status=FunctionalAssessmentStatus.UNPROVEN,
                duration_ms=duration_ms,
                inconclusive_reason=f"Evaluation timed out after {contract.timeout_seconds} seconds",
            )
        except Exception as ex:
            duration_ms = (time.perf_counter() - t_start) * 1000.0
            if proc:
                try:
                    proc.kill()
                    proc.communicate()
                except Exception:
                    pass
            return FunctionalEvaluationResult(
                contract_id=contract.contract_id,
                component_name=contract.component_name,
                target_file_rel=contract.target_file_rel,
                file_sha256=file_sha256,
                status=FunctionalAssessmentStatus.UNPROVEN,
                duration_ms=duration_ms,
                inconclusive_reason=f"Subprocess execution error: {ex}",
            )
        finally:
            # Step 9: Guaranteed cleanup of handles and temporary files
            if os.path.exists(tf_path):
                try:
                    os.remove(tf_path)
                except Exception:
                    pass
            if job_handle and os.name == "nt" and self._kernel32:
                try:
                    self._kernel32.CloseHandle(wintypes.HANDLE(job_handle))
                except Exception:
                    pass
