# -*- coding: utf-8 -*-
"""
TEITAN SELF-REPAIR - INTERNAL FUNCTIONAL ASSESSOR
Alpha implementation of generic functional defect detection for internal
self-repair components.

Key Properties:
1. Discovers registered functional contracts without knowing which component is defective.
2. Executes contracts in isolated subprocesses using Windows Job Object + timeout containment.
3. Classifies outcomes strictly as PASS, FAIL, BLOCKED, or UNPROVEN.
4. Distinguishes behavioral defects from integrity mismatches and import errors.
5. Captures target component, file path, SHA-256, runtime, and failure details.
6. Emits structured evidence compatible with RootCauseAnalyzer.
"""
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
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 PROCESS CONTAINMENT HELPERS
# ==============================================================================

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 _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_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),
    ]


def _create_bounded_job_object(memory_mb: int = 256) -> Optional[int]:
    """Creates a Windows Job Object restricting memory and process count."""
    if os.name != "nt":
        return None
    try:
        kernel32 = ctypes.windll.kernel32
        job_handle = kernel32.CreateJobObjectW(None, None)
        if not job_handle:
            return None

        JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100
        JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200
        JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
        JobObjectExtendedLimitInformation = 9

        info = _JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
        mem_bytes = memory_mb * 1024 * 1024
        info.ProcessMemoryLimit = mem_bytes
        info.JobMemoryLimit = mem_bytes
        info.BasicLimitInformation.LimitFlags = (
            JOB_OBJECT_LIMIT_PROCESS_MEMORY
            | JOB_OBJECT_LIMIT_JOB_MEMORY
            | JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
        )

        res = kernel32.SetInformationJobObject(
            job_handle,
            JobObjectExtendedLimitInformation,
            ctypes.byref(info),
            ctypes.sizeof(info),
        )
        if not res:
            kernel32.CloseHandle(job_handle)
            return None
        return job_handle
    except Exception:
        return None


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

class InternalFunctionalAssessor:
    """
    Independent functional defect evaluator.
    Discovers registered contracts and tests component behavior in isolated processes.
    """

    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)

    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 an isolated, bounded subprocess.
        """
        target_abs = os.path.join(self.workspace_root, contract.target_file_rel)
        t_start = time.perf_counter()

        # Step 1: Pre-flight checks
        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 / Compile validation
        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: Prepare isolated test harness script in temporary file
        harness_code = f"""
import sys, os
# Enforce workspace root at head of sys.path
ws = r"{self.workspace_root}"
if ws in sys.path:
    sys.path.remove(ws)
sys.path.insert(0, ws)

# Execute contract body
{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

        job_handle = None
        try:
            # Step 4: Windows Job Object containment
            job_handle = _create_bounded_job_object(memory_mb=256)

            env = os.environ.copy()
            env["PYTHONPATH"] = self.workspace_root
            # Sanitize external credentials / providers
            for sensitive_key in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "TEITAN_SECRET"]:
                env.pop(sensitive_key, None)

            proc = subprocess.Popen(
                [self.python_exe, tf_path],
                cwd=self.workspace_root,
                env=env,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )

            if job_handle and os.name == "nt":
                try:
                    ctypes.windll.kernel32.AssignProcessToJobObject(job_handle, int(proc._handle))
                except Exception:
                    pass

            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(),
                )

            # Failure occurred: extract assertion error and traceback
            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
            proc.kill()
            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
            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:
            if os.path.exists(tf_path):
                try:
                    os.remove(tf_path)
                except Exception:
                    pass
            if job_handle and os.name == "nt":
                try:
                    ctypes.windll.kernel32.CloseHandle(job_handle)
                except Exception:
                    pass

