"""Knowledge store — filesystem CRUD for ~/.fliiq/knowledge/."""

from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path

from fliiq.runtime.knowledge.models import KnowledgeRecord


class KnowledgeStore:
    """Filesystem CRUD + JSON indexes for knowledge records."""

    def __init__(self, knowledge_dir: Path | str):
        self.knowledge_dir = Path(knowledge_dir)
        self.records_dir = self.knowledge_dir / "records"
        self.index_dir = self.knowledge_dir / "index"

    @classmethod
    def from_fliiq_dir(cls) -> KnowledgeStore:
        from fliiq.runtime.config import resolve_fliiq_dir

        return cls(resolve_fliiq_dir() / "knowledge")

    def ensure_dirs(self) -> None:
        self.records_dir.mkdir(parents=True, exist_ok=True)
        self.index_dir.mkdir(parents=True, exist_ok=True)

    # ── CRUD ──────────────────────────────────────────────

    def save(self, record: KnowledgeRecord) -> KnowledgeRecord:
        """Persist a record and update indexes."""
        self.ensure_dirs()
        path = self.records_dir / f"{record.id}.json"
        path.write_text(record.model_dump_json(indent=2), encoding="utf-8")
        self._update_indexes(record)
        return record

    def load(self, record_id: str) -> KnowledgeRecord | None:
        path = self.records_dir / f"{record_id}.json"
        if not path.is_file():
            return None
        return KnowledgeRecord.model_validate_json(path.read_text(encoding="utf-8"))

    def update(self, record_id: str, updates: dict) -> KnowledgeRecord | None:
        """Apply partial updates to a record. Returns updated record or None."""
        record = self.load(record_id)
        if record is None:
            return None

        # Remove old index entries before update
        self._remove_from_indexes(record)

        for key, value in updates.items():
            if hasattr(record, key):
                setattr(record, key, value)
        record.updated_at = datetime.now(timezone.utc)

        path = self.records_dir / f"{record_id}.json"
        path.write_text(record.model_dump_json(indent=2), encoding="utf-8")
        self._update_indexes(record)
        return record

    def delete(self, record_id: str) -> bool:
        """Soft-delete by setting expired=True."""
        record = self.update(record_id, {"expired": True})
        return record is not None

    def list_records(
        self,
        limit: int = 50,
        domain: str | None = None,
        record_type: str | None = None,
        include_expired: bool = False,
    ) -> list[KnowledgeRecord]:
        """List records, optionally filtered by domain or type."""
        if not self.records_dir.is_dir():
            return []

        if domain:
            ids = self.by_domain(domain)
            records = [r for rid in ids if (r := self.load(rid)) is not None]
        else:
            records = []
            for path in sorted(self.records_dir.glob("*.json"), reverse=True):
                rec = self._load_from_path(path)
                if rec is not None:
                    records.append(rec)
                if len(records) >= limit * 2:  # over-fetch for filtering
                    break

        if record_type:
            records = [r for r in records if r.type.value == record_type]
        if not include_expired:
            records = [r for r in records if not r.expired]

        # Sort by confidence desc, then created_at desc
        records.sort(key=lambda r: (-r.confidence, r.created_at), reverse=False)
        records.sort(key=lambda r: -r.confidence)
        return records[:limit]

    def count(self) -> int:
        if not self.records_dir.is_dir():
            return 0
        return sum(1 for _ in self.records_dir.glob("*.json"))

    # ── Index lookups ─────────────────────────────────────

    def by_domain(self, domain: str) -> list[str]:
        return self._read_index("domain").get(domain, [])

    def by_tag(self, tag: str) -> list[str]:
        return self._read_index("tags").get(tag, [])

    def by_anchor_file(self, file_path: str) -> list[str]:
        return self._read_index("anchors").get(file_path, [])

    # ── Index management ──────────────────────────────────

    def rebuild_indexes(self) -> None:
        """Full rebuild from records/ directory."""
        domain_idx: dict[str, list[str]] = {}
        tags_idx: dict[str, list[str]] = {}
        anchors_idx: dict[str, list[str]] = {}

        if not self.records_dir.is_dir():
            return

        for path in self.records_dir.glob("*.json"):
            rec = self._load_from_path(path)
            if rec is None or rec.expired:
                continue
            self._add_to_index_dicts(rec, domain_idx, tags_idx, anchors_idx)

        self.ensure_dirs()
        self._write_index("domain", domain_idx)
        self._write_index("tags", tags_idx)
        self._write_index("anchors", anchors_idx)

    def _update_indexes(self, record: KnowledgeRecord) -> None:
        """Add a record to all indexes."""
        domain_idx = self._read_index("domain")
        tags_idx = self._read_index("tags")
        anchors_idx = self._read_index("anchors")

        self._add_to_index_dicts(record, domain_idx, tags_idx, anchors_idx)

        self._write_index("domain", domain_idx)
        self._write_index("tags", tags_idx)
        self._write_index("anchors", anchors_idx)

    def _remove_from_indexes(self, record: KnowledgeRecord) -> None:
        """Remove a record from all indexes."""
        domain_idx = self._read_index("domain")
        tags_idx = self._read_index("tags")
        anchors_idx = self._read_index("anchors")

        rid = record.id
        if record.domain and rid in domain_idx.get(record.domain, []):
            domain_idx[record.domain].remove(rid)
        for tag in record.tags:
            if rid in tags_idx.get(tag, []):
                tags_idx[tag].remove(rid)
        if record.anchor and rid in anchors_idx.get(record.anchor.file, []):
            anchors_idx[record.anchor.file].remove(rid)

        self._write_index("domain", domain_idx)
        self._write_index("tags", tags_idx)
        self._write_index("anchors", anchors_idx)

    @staticmethod
    def _add_to_index_dicts(
        record: KnowledgeRecord,
        domain_idx: dict[str, list[str]],
        tags_idx: dict[str, list[str]],
        anchors_idx: dict[str, list[str]],
    ) -> None:
        rid = record.id
        if record.domain:
            domain_idx.setdefault(record.domain, [])
            if rid not in domain_idx[record.domain]:
                domain_idx[record.domain].append(rid)
        for tag in record.tags:
            tags_idx.setdefault(tag, [])
            if rid not in tags_idx[tag]:
                tags_idx[tag].append(rid)
        if record.anchor:
            anchors_idx.setdefault(record.anchor.file, [])
            if rid not in anchors_idx[record.anchor.file]:
                anchors_idx[record.anchor.file].append(rid)

    def _read_index(self, name: str) -> dict[str, list[str]]:
        path = self.index_dir / f"{name}.json"
        if not path.is_file():
            # Auto-rebuild if missing
            if self.records_dir.is_dir() and any(self.records_dir.glob("*.json")):
                self.rebuild_indexes()
                if path.is_file():
                    return json.loads(path.read_text(encoding="utf-8"))
            return {}
        return json.loads(path.read_text(encoding="utf-8"))

    def _write_index(self, name: str, data: dict) -> None:
        self.ensure_dirs()
        path = self.index_dir / f"{name}.json"
        path.write_text(json.dumps(data, indent=2), encoding="utf-8")

    @staticmethod
    def _load_from_path(path: Path) -> KnowledgeRecord | None:
        try:
            return KnowledgeRecord.model_validate_json(path.read_text(encoding="utf-8"))
        except Exception:
            return None
