"""
Phase 59: Shared Knowledge Graph for the Brynq Swarm.

A dedicated knowledge base that agents can write to and query.
Entries are stored as individual JSON files with a JSONL index for fast listing.
Supports keyword search, confidence scoring, verification, and export.

Storage:
  state/broker/knowledge/{knowledge_id}.json
  state/broker/knowledge/index.jsonl
"""

import csv
import io
import json
import re
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from brynq.server import BROKER_DIR, _write_json_sync, _read_jsonl_sync, _append_jsonl_sync

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

KNOWLEDGE_DIR = BROKER_DIR / "knowledge"
KNOWLEDGE_DIR.mkdir(parents=True, exist_ok=True)

INDEX_PATH = KNOWLEDGE_DIR / "index.jsonl"

VALID_CATEGORIES = (
    "general",
    "architecture",
    "codebase",
    "process",
    "decision",
    "bug",
    "feature",
    "reference",
)


# ── Internal helpers ───────────────────────────────────────────────────────


def _read_entry(knowledge_id: str) -> Optional[dict]:
    """Read a knowledge entry by ID. Returns None if not found."""
    path = KNOWLEDGE_DIR / f"{knowledge_id}.json"
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


def _write_entry(entry: dict) -> None:
    """Write a knowledge entry to its JSON file."""
    path = KNOWLEDGE_DIR / f"{entry['knowledge_id']}.json"
    _write_json_sync(path, entry)


def _tokenize(text: str) -> set:
    """Tokenize text into lowercase words for keyword matching."""
    return set(re.findall(r"[a-z0-9]+", text.lower()))


def _score_match(query_tokens: set, entry: dict) -> float:
    """Score how relevant an entry is to a set of query tokens."""
    topic_tokens = _tokenize(entry.get("topic", ""))
    content_tokens = _tokenize(entry.get("content", ""))
    tag_tokens = set()
    for tag in entry.get("tags", []):
        tag_tokens |= _tokenize(tag)

    # Topic matches are worth more than content matches
    topic_hits = len(query_tokens & topic_tokens)
    content_hits = len(query_tokens & content_tokens)
    tag_hits = len(query_tokens & tag_tokens)

    if not query_tokens:
        return 0.0

    total = len(query_tokens)
    score = (topic_hits * 3.0 + content_hits * 1.0 + tag_hits * 2.0) / (total * 3.0)
    return min(score, 1.0)


def _rebuild_index() -> None:
    """Rebuild the index.jsonl from all entry files."""
    entries = []
    for f in KNOWLEDGE_DIR.glob("*.json"):
        if f.name == "index.jsonl":
            continue
        try:
            entry = json.loads(f.read_text("utf-8"))
            entries.append(entry)
        except (json.JSONDecodeError, OSError):
            continue

    # Overwrite index
    if INDEX_PATH.exists():
        INDEX_PATH.unlink()
    for entry in entries:
        idx = {
            "knowledge_id": entry["knowledge_id"],
            "topic": entry["topic"],
            "category": entry["category"],
            "author_id": entry["author_id"],
            "archived": entry.get("archived", False),
            "created_at": entry["created_at"],
        }
        _append_jsonl_sync(INDEX_PATH, idx)


# ── Public API ─────────────────────────────────────────────────────────────


def store_knowledge(
    topic: str,
    content: str,
    author_id: str,
    tags: Optional[List[str]] = None,
    category: str = "general",
    confidence: float = 1.0,
) -> str:
    """Add a knowledge entry. Returns the knowledge_id."""
    if category not in VALID_CATEGORIES:
        raise ValueError(f"Invalid category '{category}'. Must be one of: {VALID_CATEGORIES}")
    if not 0.0 <= confidence <= 1.0:
        raise ValueError("Confidence must be between 0.0 and 1.0")
    if not topic.strip():
        raise ValueError("Topic cannot be empty")
    if not content.strip():
        raise ValueError("Content cannot be empty")

    knowledge_id = uuid.uuid4().hex[:12]
    now = datetime.now(timezone.utc).isoformat()

    entry = {
        "knowledge_id": knowledge_id,
        "topic": topic,
        "content": content,
        "author_id": author_id,
        "tags": tags or [],
        "category": category,
        "confidence": confidence,
        "created_at": now,
        "updated_at": now,
        "archived": False,
        "revisions": [],
        "verifications": [],
    }

    _write_entry(entry)

    idx = {
        "knowledge_id": knowledge_id,
        "topic": topic,
        "category": category,
        "author_id": author_id,
        "archived": False,
        "created_at": now,
    }
    _append_jsonl_sync(INDEX_PATH, idx)

    return knowledge_id


def query_knowledge(
    query: str,
    category: Optional[str] = None,
    tags: Optional[List[str]] = None,
    min_confidence: float = 0.0,
    limit: int = 10,
) -> List[dict]:
    """Search knowledge by text query. Returns ranked results with relevance scores."""
    query_tokens = _tokenize(query)
    results = []

    for f in KNOWLEDGE_DIR.glob("*.json"):
        try:
            entry = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        if entry.get("archived", False):
            continue
        if category and entry.get("category") != category:
            continue
        if tags:
            entry_tags = set(entry.get("tags", []))
            if not set(tags) & entry_tags:
                continue
        if entry.get("confidence", 0) < min_confidence:
            continue

        score = _score_match(query_tokens, entry)
        if score > 0:
            result = dict(entry)
            result["relevance_score"] = round(score, 4)
            results.append(result)

    results.sort(key=lambda x: x["relevance_score"], reverse=True)
    return results[:limit]


def get_knowledge(knowledge_id: str) -> Optional[dict]:
    """Get a specific entry by ID."""
    entry = _read_entry(knowledge_id)
    if entry is None or entry.get("archived", False):
        return None
    return entry


def update_knowledge(
    knowledge_id: str,
    content: Optional[str] = None,
    confidence: Optional[float] = None,
    tags: Optional[List[str]] = None,
) -> Optional[dict]:
    """Update an existing entry. Tracks revision history."""
    entry = _read_entry(knowledge_id)
    if entry is None:
        raise FileNotFoundError(f"Knowledge entry '{knowledge_id}' not found")
    if entry.get("archived", False):
        raise ValueError(f"Knowledge entry '{knowledge_id}' is archived")

    now = datetime.now(timezone.utc).isoformat()

    # Save current state as revision
    revision = {
        "content": entry["content"],
        "confidence": entry["confidence"],
        "tags": list(entry["tags"]),
        "revised_at": now,
    }
    entry.setdefault("revisions", []).append(revision)

    if content is not None:
        if not content.strip():
            raise ValueError("Content cannot be empty")
        entry["content"] = content
    if confidence is not None:
        if not 0.0 <= confidence <= 1.0:
            raise ValueError("Confidence must be between 0.0 and 1.0")
        entry["confidence"] = confidence
    if tags is not None:
        entry["tags"] = tags

    entry["updated_at"] = now
    _write_entry(entry)
    return entry


def delete_knowledge(knowledge_id: str) -> bool:
    """Soft delete (mark as archived). Returns True if deleted."""
    entry = _read_entry(knowledge_id)
    if entry is None:
        return False
    if entry.get("archived", False):
        return False

    entry["archived"] = True
    entry["archived_at"] = datetime.now(timezone.utc).isoformat()
    _write_entry(entry)
    return True


def list_topics(category: Optional[str] = None) -> List[dict]:
    """List all knowledge topics, optionally filtered by category."""
    topics = []
    for f in KNOWLEDGE_DIR.glob("*.json"):
        try:
            entry = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        if entry.get("archived", False):
            continue
        if category and entry.get("category") != category:
            continue

        topics.append({
            "knowledge_id": entry["knowledge_id"],
            "topic": entry["topic"],
            "category": entry["category"],
            "confidence": entry.get("confidence", 1.0),
            "author_id": entry["author_id"],
            "created_at": entry["created_at"],
        })

    topics.sort(key=lambda x: x["created_at"], reverse=True)
    return topics


def get_context_for_task(task_description: str, limit: int = 5) -> List[dict]:
    """Find most relevant knowledge entries for a given task."""
    query_tokens = _tokenize(task_description)
    scored = []

    for f in KNOWLEDGE_DIR.glob("*.json"):
        try:
            entry = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        if entry.get("archived", False):
            continue

        score = _score_match(query_tokens, entry)
        if score > 0:
            result = dict(entry)
            result["relevance_score"] = round(score, 4)
            scored.append(result)

    scored.sort(key=lambda x: (x["relevance_score"], x.get("confidence", 0)), reverse=True)
    return scored[:limit]


def verify_knowledge(
    knowledge_id: str,
    verifier_id: str,
    is_accurate: bool = True,
) -> Optional[dict]:
    """Agents can verify/dispute knowledge entries.

    Verified entries get a confidence boost (+0.1, capped at 1.0).
    Disputed entries get a confidence reduction (-0.15, floored at 0.0).
    """
    entry = _read_entry(knowledge_id)
    if entry is None:
        raise FileNotFoundError(f"Knowledge entry '{knowledge_id}' not found")
    if entry.get("archived", False):
        raise ValueError(f"Knowledge entry '{knowledge_id}' is archived")

    now = datetime.now(timezone.utc).isoformat()

    verification = {
        "verifier_id": verifier_id,
        "is_accurate": is_accurate,
        "verified_at": now,
    }
    entry.setdefault("verifications", []).append(verification)

    if is_accurate:
        entry["confidence"] = min(entry.get("confidence", 1.0) + 0.1, 1.0)
    else:
        entry["confidence"] = max(entry.get("confidence", 1.0) - 0.15, 0.0)

    entry["updated_at"] = now
    _write_entry(entry)
    return entry


def get_knowledge_stats() -> dict:
    """Return knowledge base statistics."""
    total = 0
    archived = 0
    by_category: Dict[str, int] = {}
    contributors: Dict[str, int] = {}
    verified_count = 0
    total_active = 0

    for f in KNOWLEDGE_DIR.glob("*.json"):
        try:
            entry = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        total += 1
        if entry.get("archived", False):
            archived += 1
            continue

        total_active += 1
        cat = entry.get("category", "general")
        by_category[cat] = by_category.get(cat, 0) + 1

        author = entry.get("author_id", "unknown")
        contributors[author] = contributors.get(author, 0) + 1

        if entry.get("verifications"):
            verified_count += 1

    # Top contributors sorted by count
    top_contributors = sorted(contributors.items(), key=lambda x: x[1], reverse=True)[:10]

    verification_rate = (verified_count / total_active * 100) if total_active > 0 else 0.0

    return {
        "total_entries": total,
        "active_entries": total_active,
        "archived_entries": archived,
        "by_category": by_category,
        "top_contributors": [{"author_id": a, "count": c} for a, c in top_contributors],
        "verified_count": verified_count,
        "verification_rate": round(verification_rate, 1),
    }


def export_knowledge(
    category: Optional[str] = None,
    format: str = "json",
) -> str:
    """Export knowledge base as JSON or CSV string."""
    if format not in ("json", "csv"):
        raise ValueError(f"Invalid format '{format}'. Must be 'json' or 'csv'")

    entries = []
    for f in KNOWLEDGE_DIR.glob("*.json"):
        try:
            entry = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        if entry.get("archived", False):
            continue
        if category and entry.get("category") != category:
            continue
        entries.append(entry)

    entries.sort(key=lambda x: x.get("created_at", ""), reverse=True)

    if format == "json":
        return json.dumps(entries, indent=2, default=str)

    # CSV format
    if not entries:
        return ""

    output = io.StringIO()
    fieldnames = [
        "knowledge_id", "topic", "content", "author_id",
        "category", "confidence", "tags", "created_at", "updated_at",
    ]
    writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore")
    writer.writeheader()
    for entry in entries:
        row = dict(entry)
        row["tags"] = ";".join(entry.get("tags", []))
        writer.writerow(row)

    return output.getvalue()
