"""Knowledge graph self-test: validate entries match reality.

Checks that knowledge entries referencing file paths, version numbers,
and confidence levels are still accurate. Designed to run on-demand
via ``gm_knowledge action=validate`` or as a weekly SessionStart hook.

Usage:
    validator = KnowledgeValidator(db)
    report = validator.validate_all()
    print(report.to_dict())
"""
from __future__ import annotations

import json
import os
import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from .db import Database


# --- Data classes ---


@dataclass
class ValidationIssue:
    """A single validation finding."""
    entry_id: str
    title: str
    issue: str


@dataclass
class ValidationReport:
    """Aggregate results from all validators."""
    valid: int = 0
    invalid: int = 0
    stale: int = 0
    issues: list[ValidationIssue] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "valid": self.valid,
            "invalid": self.invalid,
            "stale": self.stale,
            "total_checked": self.valid + self.invalid + self.stale,
            "issues": [
                {"entry_id": i.entry_id, "title": i.title, "issue": i.issue}
                for i in self.issues
            ],
        }

    def merge(self, other: "ValidationReport") -> None:
        """Merge another report into this one."""
        self.valid += other.valid
        self.invalid += other.invalid
        self.stale += other.stale
        self.issues.extend(other.issues)


# --- Path extraction ---

# Matches absolute Unix paths (/foo/bar) and ~/ home-relative paths
_PATH_RE = re.compile(
    r"(?:^|[\s\"'`(,;])("           # leading context
    r"(?:~?/[A-Za-z0-9._\-]+)"      # first component (~/foo or /foo)
    r"(?:/[A-Za-z0-9._\-]+)*"       # additional /components
    r"(?:\.[a-zA-Z0-9]{1,10})?"      # optional file extension
    r")",
    re.MULTILINE,
)

# Matches version strings like "v1.4.0", "1.4.0", "version 2.3.1"
_VERSION_RE = re.compile(
    r"(?:v(?:ersion)?\s*)?(\d+\.\d+(?:\.\d+)?)",
    re.IGNORECASE,
)

# Files that commonly declare version numbers
_VERSION_FILES = {
    "package.json": lambda data: data.get("version"),
    "Cargo.toml": None,  # Handled by regex parser
    "pyproject.toml": None,  # Handled by regex parser
    "setup.cfg": None,
    "__init__.py": None,
}


def _extract_paths(text: str) -> list[str]:
    """Extract file/directory paths from text content."""
    paths = []
    for match in _PATH_RE.finditer(text):
        raw = match.group(1).strip()
        # Expand ~ to actual home directory
        expanded = os.path.expanduser(raw)
        # Filter out common false positives
        if expanded.startswith("/dev/") or expanded.startswith("/proc/"):
            continue
        if len(expanded) < 4:
            continue
        paths.append(expanded)
    return paths


def _extract_version_from_file(file_path: Path) -> str | None:
    """Try to read a version number from a known version file."""
    if not file_path.exists():
        return None
    try:
        text = file_path.read_text(encoding="utf-8", errors="ignore")
    except (OSError, PermissionError):
        return None

    name = file_path.name

    if name == "package.json":
        try:
            data = json.loads(text)
            return data.get("version")
        except (json.JSONDecodeError, KeyError):
            return None

    if name == "Cargo.toml":
        # Look for version = "x.y.z" in [package] section
        m = re.search(r'^\[package\].*?^version\s*=\s*"([^"]+)"', text,
                       re.MULTILINE | re.DOTALL)
        return m.group(1) if m else None

    if name == "pyproject.toml":
        m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
        return m.group(1) if m else None

    if name == "__init__.py":
        m = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', text)
        return m.group(1) if m else None

    return None


def _find_version_file_near(path_str: str) -> Path | None:
    """Walk up from a path to find the nearest version file."""
    p = Path(path_str).expanduser()
    # If it's a file, start from its parent
    if p.is_file():
        p = p.parent
    elif not p.exists():
        return None

    for _ in range(6):  # max 6 levels up
        for vf in _VERSION_FILES:
            candidate = p / vf
            if candidate.exists():
                return candidate
        parent = p.parent
        if parent == p:
            break
        p = parent
    return None


# --- Validator ---


class KnowledgeValidator:
    """Validate knowledge graph entries against filesystem reality."""

    # Metadata key for weekly hook tracking
    META_KEY = "validate_last_run"

    def __init__(self, db: Database):
        self.db = db

    def _load_active_entries(self) -> list[dict[str, Any]]:
        """Load all non-pruned knowledge entries."""
        return self.db.execute(
            "SELECT * FROM knowledge_entries WHERE pruned_at IS NULL "
            "ORDER BY created_at DESC"
        )

    # --- Freshness field writers ---

    def _mark_valid(self, entry_id: str) -> None:
        """Mark an entry as validated and confirmed.

        Only upgrades to 'valid' if not already marked 'stale' during
        this validation run (stale always wins over valid).
        """
        self.db.execute(
            """UPDATE knowledge_entries
               SET last_validated_at = datetime('now'),
                   last_confirmed_at = datetime('now'),
                   validation_status = 'valid'
               WHERE id = ?
                 AND (validation_status IS NULL OR validation_status != 'stale')""",
            (entry_id,),
        )

    def _mark_stale(self, entry_id: str) -> None:
        """Mark an entry as stale — always overwrites any prior status."""
        self.db.execute(
            """UPDATE knowledge_entries
               SET last_validated_at = datetime('now'),
                   stale_flagged_at = datetime('now'),
                   validation_status = 'stale'
               WHERE id = ?""",
            (entry_id,),
        )

    # --- Individual validators ---

    def validate_paths(self) -> ValidationReport:
        """Check entries referencing file paths — do those paths still exist?"""
        report = ValidationReport()
        entries = self._load_active_entries()

        for entry in entries:
            content = f"{entry.get('title', '')} {entry.get('content', '')}"
            paths = _extract_paths(content)
            if not paths:
                continue  # No paths to validate — skip, don't mark stale

            missing = []
            for p in paths:
                expanded = Path(p).expanduser()
                if not expanded.exists():
                    missing.append(p)

            if missing:
                report.invalid += 1
                self._mark_stale(entry["id"])
                # Limit displayed paths to avoid noise
                shown = missing[:5]
                suffix = f" (+{len(missing) - 5} more)" if len(missing) > 5 else ""
                report.issues.append(ValidationIssue(
                    entry_id=entry["id"],
                    title=entry.get("title", ""),
                    issue=f"Referenced paths not found: {', '.join(shown)}{suffix}",
                ))
            else:
                report.valid += 1
                self._mark_valid(entry["id"])

        return report

    def validate_versions(self) -> ValidationReport:
        """Check entries with version numbers against actual package files."""
        report = ValidationReport()
        entries = self._load_active_entries()

        for entry in entries:
            content = f"{entry.get('title', '')} {entry.get('content', '')}"

            # Extract version claims from the entry
            version_matches = _VERSION_RE.findall(content)
            if not version_matches:
                continue

            # Find paths in the entry to locate version files near them
            paths = _extract_paths(content)
            if not paths:
                continue

            # Try to find a version file near any referenced path
            actual_version = None
            version_file = None
            for p in paths:
                vf = _find_version_file_near(p)
                if vf:
                    actual_version = _extract_version_from_file(vf)
                    version_file = vf
                    if actual_version:
                        break

            if not actual_version:
                continue  # Can't verify — skip rather than false-flag

            # Check if any claimed version matches the actual version
            claimed_versions = set(version_matches)
            if actual_version not in claimed_versions:
                report.invalid += 1
                self._mark_stale(entry["id"])
                report.issues.append(ValidationIssue(
                    entry_id=entry["id"],
                    title=entry.get("title", ""),
                    issue=(
                        f"Version mismatch: entry claims {', '.join(sorted(claimed_versions))} "
                        f"but {version_file.name} has {actual_version}"
                    ),
                ))
            else:
                report.valid += 1
                self._mark_valid(entry["id"])

        return report

    def validate_stale(self, days: int = 30) -> ValidationReport:
        """Find entries with confidence <= 0.5 that haven't been reviewed in N+ days."""
        report = ValidationReport()
        cutoff = (datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")

        stale_entries = self.db.execute(
            """SELECT id, title, confidence, updated_at, created_at
               FROM knowledge_entries
               WHERE pruned_at IS NULL
                 AND confidence <= 0.5
                 AND COALESCE(updated_at, created_at) < ?
               ORDER BY COALESCE(updated_at, created_at)
               LIMIT 50""",
            (cutoff,),
        )

        for entry in stale_entries:
            report.stale += 1
            self._mark_stale(entry["id"])
            last_touch = entry.get("updated_at") or entry.get("created_at") or "unknown"
            confidence = entry.get("confidence", 0.5)
            report.issues.append(ValidationIssue(
                entry_id=entry["id"],
                title=entry.get("title", ""),
                issue=(
                    f"Stale: confidence={confidence}, "
                    f"last updated {last_touch} ({days}+ days ago)"
                ),
            ))

        # Count non-stale low-confidence entries as valid
        total_low_conf = self.db.execute_one(
            """SELECT COUNT(*) as cnt FROM knowledge_entries
               WHERE pruned_at IS NULL AND confidence <= 0.5""",
        )
        total = total_low_conf["cnt"] if total_low_conf else 0
        report.valid = max(0, total - report.stale)

        return report

    def validate_all(self) -> ValidationReport:
        """Run all validation checks and return a combined report."""
        combined = ValidationReport()
        combined.merge(self.validate_paths())
        combined.merge(self.validate_versions())
        combined.merge(self.validate_stale())
        return combined

    # --- Weekly hook support ---

    def should_run_weekly(self) -> bool:
        """Check if a weekly validation is due (7+ days since last run)."""
        try:
            row = self.db.execute_one(
                "SELECT value FROM _schema_metadata WHERE key = ?",
                (self.META_KEY,),
            )
        except Exception:
            return True  # Table might not exist yet

        if not row:
            return True

        try:
            last_run = datetime.fromisoformat(row["value"])
            return datetime.now(timezone.utc).replace(tzinfo=None) - last_run > timedelta(days=7)
        except (ValueError, TypeError):
            return True

    def _ensure_metadata_table(self) -> None:
        """Create the _schema_metadata table if it doesn't exist."""
        try:
            self.db.execute(
                "CREATE TABLE IF NOT EXISTS _schema_metadata "
                "(key TEXT PRIMARY KEY, value TEXT, "
                "updated_at TEXT DEFAULT (datetime('now')))"
            )
        except Exception:
            pass

    def record_run(self) -> None:
        """Record that a validation run just completed."""
        now = datetime.now(timezone.utc).replace(tzinfo=None).isoformat()
        self._ensure_metadata_table()
        try:
            self.db.execute(
                "INSERT INTO _schema_metadata (key, value) VALUES (?, ?) "
                "ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')",
                (self.META_KEY, now, now),
            )
        except Exception:
            pass  # Non-critical — don't break startup

    def run_weekly_if_due(self) -> ValidationReport | None:
        """Run validation if 7+ days since last run. Returns report or None."""
        if not self.should_run_weekly():
            return None
        report = self.validate_all()
        self.record_run()
        return report
