"""
map_collector.py - System Layout Data Collector

WO-ONTOLOGY-HARDENING-01: Collects structured data about "what is plugged in where"
for the /echo/map endpoint.

This module provides observability for Echo's architecture without implying
self-awareness. It's about invariants + receipts + fingerprints.

Data collected:
- Git state (head, branch, dirty)
- Port status (basin_server, helixd)
- Backend availability (decode_only, chat, qpu_stubs)
- Module load status
- Tripwire defaults
- Recent receipts summary
- Key file paths
"""

from __future__ import annotations

import hashlib
import importlib
import json
import os
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from .helpers import PROJECT_ROOT


# ============================================================================
# Git State
# ============================================================================

def get_git_state() -> Dict[str, Any]:
    """Get current git state (head, branch, dirty)."""
    result = {
        "head": None,
        "branch": None,
        "dirty": None,
    }

    try:
        # Get HEAD commit hash
        head_result = subprocess.run(
            ["git", "rev-parse", "--short", "HEAD"],
            cwd=PROJECT_ROOT,
            capture_output=True,
            text=True,
            timeout=5,
        )
        if head_result.returncode == 0:
            result["head"] = head_result.stdout.strip()

        # Get current branch
        branch_result = subprocess.run(
            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
            cwd=PROJECT_ROOT,
            capture_output=True,
            text=True,
            timeout=5,
        )
        if branch_result.returncode == 0:
            result["branch"] = branch_result.stdout.strip()

        # Check if dirty (uncommitted changes)
        dirty_result = subprocess.run(
            ["git", "status", "--porcelain"],
            cwd=PROJECT_ROOT,
            capture_output=True,
            text=True,
            timeout=10,
        )
        if dirty_result.returncode == 0:
            result["dirty"] = bool(dirty_result.stdout.strip())

    except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
        pass

    return result


# ============================================================================
# Port Status
# ============================================================================

def check_port_health(host: str, port: int, path: str = "/health") -> Dict[str, Any]:
    """Check if a service is healthy on a given port."""
    import urllib.request
    import urllib.error
    import json

    result = {
        "port": port,
        "status": "not_running",
        "backend": None,
    }

    try:
        url = f"http://{host}:{port}{path}"
        with urllib.request.urlopen(url, timeout=2) as response:
            if response.status == 200:
                result["status"] = "healthy"
                try:
                    data = json.loads(response.read().decode('utf-8'))
                    result["backend"] = data.get("backend", data.get("version"))
                except (json.JSONDecodeError, ValueError):
                    pass
    except (urllib.error.URLError, ConnectionRefusedError, TimeoutError, OSError):
        result["status"] = "not_running"
    except Exception:
        result["status"] = "error"

    return result


def get_ports_status() -> Dict[str, Dict[str, Any]]:
    """Get status of known service ports."""
    return {
        "basin_server": check_port_health("localhost", 8001, "/health"),
        "helixd": check_port_health("localhost", 8765, "/healthcheck"),
    }


# ============================================================================
# Backend Availability
# ============================================================================

def check_decode_only_backends() -> Dict[str, bool]:
    """Check availability of decode-only backends."""
    backends = {}

    # HelixCode v0
    try:
        from helix_cdc.regrow.helixcode_v0 import HelixCodeBlock
        backends["helixcode_v0"] = True
    except ImportError:
        backends["helixcode_v0"] = False

    # CDNA streaming
    try:
        from helix_cdc.regrow.cdna_stream import CDNAStreamReader
        backends["cdna_streaming"] = True
    except ImportError:
        backends["cdna_streaming"] = False

    # Stream X@W matmul
    try:
        from helix_cdc.regrow.stream_xw_matmul import stream_xw_matmul
        backends["stream_xw_matmul"] = True
    except ImportError:
        backends["stream_xw_matmul"] = False

    return backends


def check_chat_backends() -> Dict[str, bool]:
    """Check availability of chat backends."""
    backends = {}

    # llama_cpp
    try:
        import llama_cpp
        backends["llama_cpp"] = True
    except ImportError:
        backends["llama_cpp"] = False

    # Basin server module
    try:
        from api.basin_server import app
        backends["basin_server"] = True
    except ImportError:
        backends["basin_server"] = False

    return backends


def check_qpu_stubs() -> bool:
    """Check if QPU stubs are available."""
    try:
        from helix_cdc.qpu_bridge import QPUBridge
        return True
    except ImportError:
        return False


def get_backends_status() -> Dict[str, Any]:
    """Get status of all backends."""
    return {
        "decode_only": check_decode_only_backends(),
        "chat": check_chat_backends(),
        "qpu_stubs": check_qpu_stubs(),
    }


# ============================================================================
# Module Load Status
# ============================================================================

def check_module_loaded(module_name: str) -> str:
    """Check if a module can be imported."""
    try:
        importlib.import_module(module_name)
        return "loaded"
    except ImportError as e:
        return f"import_error: {str(e)[:50]}"
    except Exception as e:
        return f"error: {str(e)[:50]}"


def get_modules_status() -> Dict[str, str]:
    """Check load status of key modules."""
    key_modules = [
        "helix_cdc",
        "helix_cdc.regrow",
        "helix_cdc.echo.tools",
        "echo_cog",
        "flowtorch",
    ]

    return {module: check_module_loaded(module) for module in key_modules}


# ============================================================================
# Tripwire Defaults
# ============================================================================

def get_tripwire_defaults() -> Dict[str, Any]:
    """Get tripwire-related environment defaults."""
    return {
        "inference_tripwire_enabled": True,  # Default is enabled
        "helix_allow_foreign_inference": os.environ.get("HELIX_ALLOW_FOREIGN_INFERENCE", ""),
    }


# ============================================================================
# Receipts Summary
# ============================================================================

def get_receipts_summary() -> Dict[str, Any]:
    """Get summary of recent receipts."""
    receipts_dir = PROJECT_ROOT / "receipts"
    result = {
        "last_7_days": {"pass": 0, "fail": 0},
        "newest": None,
    }

    if not receipts_dir.exists():
        return result

    cutoff = datetime.now(timezone.utc) - timedelta(days=7)
    newest_time = None
    newest_path = None

    # Walk receipts directory
    for receipt_path in receipts_dir.rglob("*.json"):
        try:
            mtime = datetime.fromtimestamp(receipt_path.stat().st_mtime, tz=timezone.utc)

            # Track newest
            if newest_time is None or mtime > newest_time:
                newest_time = mtime
                newest_path = receipt_path

            # Count recent receipts
            if mtime >= cutoff:
                content = receipt_path.read_text(encoding='utf-8', errors='ignore')
                if '"status": "PASS"' in content or '"status":"PASS"' in content:
                    result["last_7_days"]["pass"] += 1
                elif '"status": "FAIL"' in content or '"status":"FAIL"' in content:
                    result["last_7_days"]["fail"] += 1
        except (OSError, IOError):
            continue

    if newest_path:
        try:
            result["newest"] = str(newest_path.relative_to(PROJECT_ROOT))
        except ValueError:
            result["newest"] = str(newest_path)

    return result


# ============================================================================
# Key Paths
# ============================================================================

def get_key_paths() -> Dict[str, str]:
    """Get paths to key system files."""
    return {
        "decode_benchmarks": "tools/bench_helixcode*.py",
        "chat_backend": "echo_cog/backend_llama_cpp.py",
        "tripwire": "helix_cdc/regrow/inference_tripwire.py",
        "ontology_lint": "tools/ontology_lint.py",
        "basin_server": "api/basin_server.py",
        "echo_ops": "helix_cdc/echo/ops/endpoints.py",
    }


# ============================================================================
# Proof Pointers (WO-COAUTHOR-LOOP-HARDEN-01)
# ============================================================================

def sha256_file(path: Path) -> str:
    """Compute SHA256 of file contents, truncated to 16 hex chars."""
    if not path.exists():
        return "FILE_NOT_FOUND"
    try:
        return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
    except Exception:
        return "READ_ERROR"


def sha256_tail(path: Path, lines: int = 100) -> str:
    """Compute SHA256 of last N lines of file, truncated to 16 hex chars."""
    if not path.exists():
        return "FILE_NOT_FOUND"
    try:
        content = path.read_text(encoding='utf-8', errors='replace').strip()
        tail_lines = content.split('\n')[-lines:]
        return hashlib.sha256('\n'.join(tail_lines).encode()).hexdigest()[:16]
    except Exception:
        return "READ_ERROR"


def get_newest_receipt_sha256(receipts_dir: Path) -> str:
    """Get SHA256 of the most recently modified receipt file."""
    if not receipts_dir.exists():
        return "NO_RECEIPTS_DIR"

    newest_path = None
    newest_mtime = 0.0

    for receipt_path in receipts_dir.rglob("*.json"):
        try:
            mtime = receipt_path.stat().st_mtime
            if mtime > newest_mtime:
                newest_mtime = mtime
                newest_path = receipt_path
        except (OSError, IOError):
            continue

    if newest_path is None:
        return "NO_RECEIPTS"

    return sha256_file(newest_path)


def get_ontology_lint_status() -> Dict[str, Optional[str]]:
    """
    Get status of last ontology lint run.

    Checks coauthor_loop.jsonl for the most recent lint status.
    If no coauthor_loop exists, returns None values.
    """
    coauthor_loop_path = PROJECT_ROOT / "receipts" / "coauthor_loop.jsonl"
    result = {
        "ontology_lint_last_run": None,
        "ontology_lint_status": None,
    }

    if not coauthor_loop_path.exists():
        return result

    try:
        content = coauthor_loop_path.read_text(encoding='utf-8', errors='replace').strip()
        if not content:
            return result

        # Get last line (most recent coauthor loop run)
        lines = content.split('\n')
        for line in reversed(lines):
            if not line.strip():
                continue
            try:
                data = json.loads(line)
                if "coauthor_loop" in data:
                    # The coauthor_loop value is the lint status
                    result["ontology_lint_status"] = data.get("coauthor_loop", "UNKNOWN")
                    # Use the timestamp of when the loop ran
                    result["ontology_lint_last_run"] = data.get("timestamp")
                    break
            except json.JSONDecodeError:
                continue
    except Exception:
        pass

    return result


def collect_proof_pointers() -> Dict[str, Any]:
    """
    Collect SHA256 proof pointers for critical files.

    WO-COAUTHOR-LOOP-HARDEN-01: Make /echo/map structurally impossible to silently lie.
    """
    ontology_status = get_ontology_lint_status()

    return {
        "claude_md_sha256": sha256_file(PROJECT_ROOT / "CLAUDE.md"),
        "tripwire_sha256": sha256_file(PROJECT_ROOT / "helix_cdc" / "regrow" / "inference_tripwire.py"),
        "newest_receipt_sha256": get_newest_receipt_sha256(PROJECT_ROOT / "receipts"),
        "coauthor_trace_sha256": sha256_tail(PROJECT_ROOT / "receipts" / "coauthor_trace.jsonl", lines=100),
        "ontology_lint_last_run": ontology_status["ontology_lint_last_run"],
        "ontology_lint_status": ontology_status["ontology_lint_status"],
    }


def validate_proof_pointers(proof_pointers: Dict[str, Any]) -> Dict[str, Any]:
    """
    Validate proof pointers - if any sensor is unplugged, report it.

    WO-COAUTHOR-LOOP-HARDEN-01: "If a sensor is unplugged, the panel screams."

    Returns:
        - integrity: "ok" | "degraded" | "broken"
        - missing: list of missing/invalid pointer keys
        - valid_count: number of valid pointers
    """
    invalid_values = {"FILE_NOT_FOUND", "READ_ERROR", "NO_RECEIPTS", "NO_RECEIPTS_DIR", None}

    required_pointers = [
        "claude_md_sha256",
        "tripwire_sha256",
        "newest_receipt_sha256",
        "coauthor_trace_sha256",
    ]

    missing = []
    valid_count = 0

    for key in required_pointers:
        value = proof_pointers.get(key)
        if value in invalid_values:
            missing.append(key)
        else:
            valid_count += 1

    if len(missing) >= 3:
        integrity = "broken"
    elif missing:
        integrity = "degraded"
    else:
        integrity = "ok"

    return {
        "integrity": integrity,
        "missing": missing,
        "valid_count": valid_count,
        "total_required": len(required_pointers),
    }


# ============================================================================
# Main Collector
# ============================================================================

async def collect_system_map() -> Dict[str, Any]:
    """
    Collect complete system layout information.

    Returns structured JSON for /echo/map endpoint.

    WO-COAUTHOR-LOOP-HARDEN-01: Now includes proof_pointers and integrity validation.
    "If a sensor is unplugged, the panel screams."
    """
    timestamp = datetime.now(timezone.utc).isoformat()

    # Collect proof pointers
    proof_pointers = collect_proof_pointers()

    # Validate integrity - if sensors are missing, report it
    integrity = validate_proof_pointers(proof_pointers)

    return {
        "timestamp": timestamp,
        "git": get_git_state(),
        "ports": get_ports_status(),
        "backends": get_backends_status(),
        "modules": get_modules_status(),
        "tripwire_defaults": get_tripwire_defaults(),
        "receipts_summary": get_receipts_summary(),
        "key_paths": get_key_paths(),
        "proof_pointers": proof_pointers,
        "map_integrity": integrity,
    }
