"""
Phase 22: The Oroboros Protocol — Self-Building Infrastructure.

Brynq uses its own platform to improve itself. The product builds the product.

How it works:
  1. SCAN — Oroboros analyzes the codebase for gaps, bugs, missing tests,
     performance issues, and improvement opportunities.
  2. POST — It creates structured improvement tasks on the broker job board
     with clear specs, acceptance criteria, and required skills.
  3. ASSIGN — Available agents bid on tasks via the existing swarm system.
     Best-match agent wins based on skill + reputation.
  4. EXECUTE — The winning agent builds the improvement.
  5. REVIEW — A separate agent reviews the work (min 1 peer review).
  6. RECORD — Completed improvements are logged to the evolution ledger,
     tracking Brynq's self-improvement over time.

The human CEO retains veto power over all self-improvement cycles (Law 26).

Storage:
  state/broker/oroboros/
    cycles/{cycle_id}.json  — improvement cycle records
    ledger.jsonl            — evolution log (append-only)
    scan_results.json       — latest scan findings
"""

import json
import os
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    CHANNELS_DIR,
    SESSION_ID,
    SESSION_NAME,
    PLATFORM,
    _append_jsonl_sync,
    _write_json_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

OROBOROS_DIR = BROKER_DIR / "oroboros"
CYCLES_DIR = OROBOROS_DIR / "cycles"
LEDGER_FILE = OROBOROS_DIR / "ledger.jsonl"
SCAN_FILE = OROBOROS_DIR / "scan_results.json"

for _d in (OROBOROS_DIR, CYCLES_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# ── Scan Categories ────────────────────────────────────────────────────────

SCAN_CATEGORIES = [
    "missing_tests",
    "dead_code",
    "security_issue",
    "performance",
    "documentation",
    "refactoring",
    "feature_gap",
    "dependency_update",
]

VALID_CYCLE_STATUSES = [
    "proposed",     # Scan found an issue, task not yet posted
    "posted",       # Task posted to job board
    "claimed",      # Agent accepted the task
    "in_review",    # Work done, under peer review
    "approved",     # Review passed
    "rejected",     # Review failed — needs rework
    "completed",    # Merged and verified
    "vetoed",       # Human CEO blocked it
]


# ── Scanning ───────────────────────────────────────────────────────────────


def scan_codebase(source_dir: str = "src/brynq") -> dict:
    """Analyze the codebase and identify improvement opportunities.

    Returns a scan report with categorized findings.
    """
    findings: list[dict] = []
    src = Path(source_dir)

    if not src.exists():
        return {"error": f"Source directory '{source_dir}' not found", "findings": []}

    py_files = list(src.rglob("*.py"))
    test_files = list(Path("tests").rglob("*.py")) if Path("tests").exists() else []

    # Collect module names from source
    module_names = set()
    for f in py_files:
        if f.name.startswith("_"):
            continue
        module_names.add(f.stem)

    # Collect tested module names (rough heuristic: imports in test files)
    tested_modules = set()
    for tf in test_files:
        try:
            content = tf.read_text("utf-8", errors="replace")
            for mod in module_names:
                if f"brynq.{mod}" in content or f"from brynq import {mod}" in content:
                    tested_modules.add(mod)
        except OSError:
            continue

    # Finding: untested modules
    untested = module_names - tested_modules
    for mod in sorted(untested):
        findings.append({
            "category": "missing_tests",
            "severity": "medium",
            "target": f"src/brynq/{mod}.py",
            "description": f"Module '{mod}' has no test coverage",
            "suggested_task": f"Write comprehensive tests for brynq.{mod}",
            "skills_required": ["python", "testing"],
        })

    # Finding: large files that may need refactoring
    for f in py_files:
        try:
            lines = f.read_text("utf-8", errors="replace").count("\n")
            if lines > 500:
                findings.append({
                    "category": "refactoring",
                    "severity": "low",
                    "target": str(f),
                    "description": f"Large file ({lines} lines) — consider splitting",
                    "suggested_task": f"Refactor {f.name} into smaller modules",
                    "skills_required": ["python", "architecture"],
                })
        except OSError:
            continue

    # Finding: TODO/FIXME/HACK comments
    for f in py_files:
        try:
            for i, line in enumerate(f.read_text("utf-8", errors="replace").splitlines(), 1):
                line_upper = line.upper()
                for marker in ("TODO", "FIXME", "HACK", "XXX"):
                    if marker in line_upper and "#" in line:
                        findings.append({
                            "category": "feature_gap" if marker == "TODO" else "refactoring",
                            "severity": "low",
                            "target": f"{f}:{i}",
                            "description": line.strip(),
                            "suggested_task": f"Address {marker} at {f.name}:{i}",
                            "skills_required": ["python"],
                        })
        except OSError:
            continue

    # Finding: security — hardcoded secrets patterns
    secret_patterns = ["password=", "secret=", "token=", "api_key="]
    for f in py_files:
        try:
            content = f.read_text("utf-8", errors="replace")
            for i, line in enumerate(content.splitlines(), 1):
                stripped = line.strip().lower()
                if stripped.startswith("#"):
                    continue
                for pat in secret_patterns:
                    if pat in stripped and "os.getenv" not in stripped and "environ" not in stripped:
                        # Skip common false positives
                        if "def " in stripped or "param" in stripped or '""' in line or "''" in line:
                            continue
                        findings.append({
                            "category": "security_issue",
                            "severity": "high",
                            "target": f"{f}:{i}",
                            "description": f"Potential hardcoded secret: {pat.rstrip('=')}",
                            "suggested_task": f"Move secret to env var at {f.name}:{i}",
                            "skills_required": ["python", "security"],
                        })
        except OSError:
            continue

    report = {
        "scan_id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "scanned_by": f"{SESSION_NAME}@{PLATFORM}",
        "source_dir": source_dir,
        "total_modules": len(module_names),
        "total_findings": len(findings),
        "findings_by_category": {},
        "findings_by_severity": {},
        "findings": findings,
    }

    # Aggregate counts
    for f in findings:
        cat = f["category"]
        sev = f["severity"]
        report["findings_by_category"][cat] = report["findings_by_category"].get(cat, 0) + 1
        report["findings_by_severity"][sev] = report["findings_by_severity"].get(sev, 0) + 1

    # Persist latest scan
    _write_json_sync(SCAN_FILE, report)

    return report


def get_latest_scan() -> Optional[dict]:
    """Return the most recent scan results."""
    try:
        return json.loads(SCAN_FILE.read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return None


# ── Improvement Cycles ─────────────────────────────────────────────────────


def propose_improvement(
    finding: dict,
    title: Optional[str] = None,
    priority: str = "normal",
) -> dict:
    """Create an improvement cycle from a scan finding.

    Args:
        finding: A finding dict from scan_codebase().
        title: Override the auto-generated title.
        priority: low, normal, high, urgent.

    Returns:
        The cycle record.
    """
    cycle_id = uuid.uuid4().hex[:12]
    now = datetime.now(timezone.utc).isoformat()

    cycle = {
        "cycle_id": cycle_id,
        "status": "proposed",
        "title": title or finding.get("suggested_task", "Untitled improvement"),
        "category": finding.get("category", "feature_gap"),
        "severity": finding.get("severity", "medium"),
        "target": finding.get("target", ""),
        "description": finding.get("description", ""),
        "skills_required": finding.get("skills_required", ["python"]),
        "priority": priority,
        "proposed_by": f"{SESSION_NAME}@{PLATFORM}",
        "proposed_at": now,
        "task_id": None,
        "assigned_to": None,
        "reviewer": None,
        "review_result": None,
        "review_notes": "",
        "completed_at": None,
        "vetoed_by": None,
        "veto_reason": None,
    }

    _write_json_sync(CYCLES_DIR / f"{cycle_id}.json", cycle)
    return cycle


def post_improvement_task(cycle_id: str, channel: str = "tasks") -> dict:
    """Post an improvement cycle as a task on the broker job board.

    This bridges Oroboros into the existing task/job system.
    """
    cycle = _load_cycle(cycle_id)
    if not cycle:
        return {"error": f"Cycle {cycle_id} not found"}

    if cycle["status"] != "proposed":
        return {"error": f"Cycle {cycle_id} is '{cycle['status']}', must be 'proposed'"}

    # Create a task via the tasks module
    from brynq.tasks import create_task
    task_id = create_task(
        channel=channel,
        title=f"[OROBOROS] {cycle['title']}",
        description=(
            f"Category: {cycle['category']}\n"
            f"Target: {cycle['target']}\n"
            f"Severity: {cycle['severity']}\n"
            f"Description: {cycle['description']}\n"
            f"\nAcceptance criteria:\n"
            f"1. Fix/implement the described issue\n"
            f"2. All existing tests must still pass\n"
            f"3. Add tests for new/changed code\n"
            f"4. Peer review required before completion"
        ),
        priority=cycle["priority"],
    )

    cycle["status"] = "posted"
    cycle["task_id"] = task_id
    _save_cycle(cycle)

    # Log to evolution ledger
    _log_event(cycle_id, "posted", f"Task {task_id} posted to #{channel}")

    return {"cycle_id": cycle_id, "task_id": task_id, "status": "posted"}


def claim_improvement(cycle_id: str, agent_name: Optional[str] = None) -> dict:
    """Mark a cycle as claimed by the current agent."""
    cycle = _load_cycle(cycle_id)
    if not cycle:
        return {"error": f"Cycle {cycle_id} not found"}

    if cycle["status"] != "posted":
        return {"error": f"Cycle is '{cycle['status']}', must be 'posted'"}

    assignee = agent_name or f"{SESSION_NAME}@{PLATFORM}"
    cycle["status"] = "claimed"
    cycle["assigned_to"] = assignee
    _save_cycle(cycle)

    _log_event(cycle_id, "claimed", f"Claimed by {assignee}")
    return {"cycle_id": cycle_id, "assigned_to": assignee, "status": "claimed"}


def submit_for_review(cycle_id: str, work_summary: str) -> dict:
    """Submit completed work for peer review."""
    cycle = _load_cycle(cycle_id)
    if not cycle:
        return {"error": f"Cycle {cycle_id} not found"}

    if cycle["status"] != "claimed":
        return {"error": f"Cycle is '{cycle['status']}', must be 'claimed'"}

    cycle["status"] = "in_review"
    cycle["work_summary"] = work_summary
    _save_cycle(cycle)

    _log_event(cycle_id, "submitted_for_review", work_summary[:200])

    # Notify #tasks channel
    _append_jsonl_sync(CHANNELS_DIR / "tasks.jsonl", {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": "oroboros",
        "platform": "brynq",
        "channel": "tasks",
        "msg_type": "request",
        "message": (
            f"[OROBOROS REVIEW NEEDED] Cycle {cycle_id}: {cycle['title']}\n"
            f"Builder: {cycle['assigned_to']}\n"
            f"Summary: {work_summary[:300]}\n"
            f"Any agent with 'code_review' capability — please review."
        ),
    })

    return {"cycle_id": cycle_id, "status": "in_review"}


def review_improvement(
    cycle_id: str,
    approved: bool,
    notes: str = "",
    reviewer_name: Optional[str] = None,
) -> dict:
    """Review a submitted improvement. Requires a different agent than the builder."""
    cycle = _load_cycle(cycle_id)
    if not cycle:
        return {"error": f"Cycle {cycle_id} not found"}

    if cycle["status"] != "in_review":
        return {"error": f"Cycle is '{cycle['status']}', must be 'in_review'"}

    reviewer = reviewer_name or f"{SESSION_NAME}@{PLATFORM}"

    # Prevent self-review
    if reviewer == cycle.get("assigned_to"):
        return {"error": "Cannot review your own work (Law 43: no self-approve)"}

    cycle["reviewer"] = reviewer
    cycle["review_result"] = "approved" if approved else "rejected"
    cycle["review_notes"] = notes
    cycle["status"] = "approved" if approved else "rejected"
    _save_cycle(cycle)

    action = "approved" if approved else "rejected"
    _log_event(cycle_id, action, f"Reviewer: {reviewer}. {notes}")

    return {"cycle_id": cycle_id, "status": cycle["status"], "reviewer": reviewer}


def complete_improvement(cycle_id: str) -> dict:
    """Mark an approved improvement as completed and merged."""
    cycle = _load_cycle(cycle_id)
    if not cycle:
        return {"error": f"Cycle {cycle_id} not found"}

    if cycle["status"] != "approved":
        return {"error": f"Cycle is '{cycle['status']}', must be 'approved'"}

    cycle["status"] = "completed"
    cycle["completed_at"] = datetime.now(timezone.utc).isoformat()
    _save_cycle(cycle)

    _log_event(cycle_id, "completed", f"Improvement merged: {cycle['title']}")
    return {"cycle_id": cycle_id, "status": "completed"}


def veto_improvement(cycle_id: str, reason: str) -> dict:
    """Human CEO vetoes an improvement cycle (Law 26: human supremacy)."""
    cycle = _load_cycle(cycle_id)
    if not cycle:
        return {"error": f"Cycle {cycle_id} not found"}

    cycle["status"] = "vetoed"
    cycle["vetoed_by"] = f"{SESSION_NAME}@{PLATFORM}"
    cycle["veto_reason"] = reason
    _save_cycle(cycle)

    _log_event(cycle_id, "vetoed", f"Vetoed by CEO: {reason}")
    return {"cycle_id": cycle_id, "status": "vetoed", "reason": reason}


# ── Queries ────────────────────────────────────────────────────────────────


def list_cycles(
    status: Optional[str] = None,
    category: Optional[str] = None,
    limit: int = 50,
) -> list[dict]:
    """List improvement cycles, optionally filtered."""
    cycles = []
    for f in sorted(CYCLES_DIR.glob("*.json"), reverse=True):
        try:
            c = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if status and c.get("status") != status:
            continue
        if category and c.get("category") != category:
            continue
        cycles.append(c)
        if len(cycles) >= limit:
            break
    return cycles


def get_cycle(cycle_id: str) -> Optional[dict]:
    """Get a single improvement cycle by ID."""
    return _load_cycle(cycle_id)


def get_evolution_stats() -> dict:
    """Get aggregate stats on Brynq's self-improvement history."""
    cycles = list_cycles(limit=1000)
    stats = {
        "total_cycles": len(cycles),
        "by_status": {},
        "by_category": {},
        "completion_rate": 0.0,
        "avg_cycle_time_hours": None,
    }

    completed_times = []
    for c in cycles:
        s = c.get("status", "unknown")
        cat = c.get("category", "unknown")
        stats["by_status"][s] = stats["by_status"].get(s, 0) + 1
        stats["by_category"][cat] = stats["by_category"].get(cat, 0) + 1

        if s == "completed" and c.get("proposed_at") and c.get("completed_at"):
            try:
                t0 = datetime.fromisoformat(c["proposed_at"])
                t1 = datetime.fromisoformat(c["completed_at"])
                completed_times.append((t1 - t0).total_seconds() / 3600)
            except (ValueError, TypeError):
                pass

    total = stats["total_cycles"]
    completed = stats["by_status"].get("completed", 0)
    stats["completion_rate"] = round(completed / max(total, 1), 3)

    if completed_times:
        stats["avg_cycle_time_hours"] = round(
            sum(completed_times) / len(completed_times), 2
        )

    return stats


def get_evolution_ledger(limit: int = 50) -> list[dict]:
    """Read recent entries from the evolution ledger."""
    try:
        entries = []
        with open(LEDGER_FILE, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    try:
                        entries.append(json.loads(line))
                    except json.JSONDecodeError:
                        continue
        return entries[-limit:]
    except (FileNotFoundError, OSError):
        return []


# ── Auto-Pilot ─────────────────────────────────────────────────────────────


def run_oroboros_cycle(
    source_dir: str = "src/brynq",
    max_tasks: int = 5,
    min_severity: str = "medium",
    channel: str = "tasks",
) -> dict:
    """Run a full Oroboros scan-and-post cycle.

    Scans the codebase, filters findings by severity, and posts up to
    max_tasks improvement tasks to the job board.

    Returns summary of what was posted.
    """
    severity_rank = {"low": 0, "medium": 1, "high": 2, "critical": 3}
    min_rank = severity_rank.get(min_severity, 1)

    # Phase 102: The Swarm Feedback Loop
    try:
        from brynq.learning_feedback import run_feedback_cycle
        feedback_results = run_feedback_cycle()
        import logging
        logging.getLogger(__name__).info(f"Feedback cycle completed: {feedback_results}")
    except Exception as e:
        import logging
        logging.getLogger(__name__).error(f"Failed to run feedback cycle: {e}")

    report = scan_codebase(source_dir)
    findings = report.get("findings", [])
    if "error" in report:
        return report

    # Step 2: Filter by severity
    findings = [
        f for f in report.get("findings", [])
        if severity_rank.get(f.get("severity", "low"), 0) >= min_rank
    ]

    # Step 3: Sort by severity (highest first)
    findings.sort(
        key=lambda f: severity_rank.get(f.get("severity", "low"), 0),
        reverse=True,
    )

    # Step 4: Propose and post
    posted = []
    for finding in findings[:max_tasks]:
        cycle = propose_improvement(finding)
        result = post_improvement_task(cycle["cycle_id"], channel=channel)
        if "error" not in result:
            posted.append(result)

    _log_event("auto", "oroboros_cycle", f"Scanned {report['total_modules']} modules, posted {len(posted)} tasks")

    return {
        "scan_id": report["scan_id"],
        "total_findings": report["total_findings"],
        "filtered_findings": len(findings),
        "tasks_posted": len(posted),
        "posted": posted,
    }


# ── Internals ──────────────────────────────────────────────────────────────


def _load_cycle(cycle_id: str) -> Optional[dict]:
    path = CYCLES_DIR / f"{cycle_id}.json"
    try:
        return json.loads(path.read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return None


def _save_cycle(cycle: dict) -> None:
    _write_json_sync(CYCLES_DIR / f"{cycle['cycle_id']}.json", cycle)


def _log_event(cycle_id: str, event: str, detail: str) -> None:
    entry = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "cycle_id": cycle_id,
        "event": event,
        "detail": detail,
        "agent": f"{SESSION_NAME}@{PLATFORM}",
    }
    _append_jsonl_sync(LEDGER_FILE, entry)
