"""Knowledge base storage and retrieval."""

from __future__ import annotations

import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any

from database import Database
from logging_config import get_logger

log = get_logger(__name__)


@dataclass
class KnowledgeItem:
    """A single knowledge base item."""

    id: str
    title: str
    source_type: str  # 'url' or 'pdf'
    source_url: str | None
    source_filename: str | None
    file_path: str | None
    raw_text: str
    summary: str
    key_points: list[str]
    tags: list[str]
    status: str  # 'pending', 'processing', 'ready', 'error'
    error_message: str | None
    char_count: int
    created_at: str
    updated_at: str
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "title": self.title,
            "source_type": self.source_type,
            "source_url": self.source_url,
            "source_filename": self.source_filename,
            "file_path": self.file_path,
            "raw_text": self.raw_text,
            "summary": self.summary,
            "key_points": self.key_points,
            "tags": self.tags,
            "status": self.status,
            "error_message": self.error_message,
            "char_count": self.char_count,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "metadata": self.metadata,
        }

    def to_summary_dict(self) -> dict[str, Any]:
        """Summary without raw_text (for list endpoints)."""
        return {
            "id": self.id,
            "title": self.title,
            "source_type": self.source_type,
            "source_url": self.source_url,
            "source_filename": self.source_filename,
            "summary": self.summary,
            "key_points": self.key_points,
            "tags": self.tags,
            "status": self.status,
            "error_message": self.error_message,
            "char_count": self.char_count,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
        }


@dataclass
class KnowledgeConnection:
    """A semantic connection between two knowledge items."""

    id: str
    source_id: str
    target_id: str
    label: str
    strength: float
    created_at: str

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "source_id": self.source_id,
            "target_id": self.target_id,
            "label": self.label,
            "strength": self.strength,
            "created_at": self.created_at,
        }


PROCUREMENT_ENTITY_TYPES = {
    "supplier", "contract", "purchase_order", "spend_category",
    "process", "system", "regulation", "kpi", "organization", "role",
}


@dataclass
class KnowledgeEntity:
    """A named entity extracted from a knowledge item."""

    id: str
    item_id: str
    entity_type: str  # one of PROCUREMENT_ENTITY_TYPES
    name: str
    description: str
    properties: dict[str, Any] = field(default_factory=dict)
    created_at: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "item_id": self.item_id,
            "entity_type": self.entity_type,
            "name": self.name,
            "description": self.description,
            "properties": self.properties,
            "created_at": self.created_at,
        }


@dataclass
class KnowledgeEntityConnection:
    """A relationship between two knowledge entities."""

    id: str
    source_id: str
    target_id: str
    label: str
    strength: float
    created_at: str

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "source_id": self.source_id,
            "target_id": self.target_id,
            "label": self.label,
            "strength": self.strength,
            "created_at": self.created_at,
        }


class KnowledgeStore:
    """SQLite-backed knowledge base persistence."""

    def __init__(self, db: Database) -> None:
        self._db = db
        self._log = log.bind(component="knowledge_store")

    @staticmethod
    def _row_to_item(row: Any) -> KnowledgeItem:
        return KnowledgeItem(
            id=row["id"],
            title=row["title"],
            source_type=row["source_type"],
            source_url=row["source_url"],
            source_filename=row["source_filename"],
            file_path=row["file_path"],
            raw_text=row["raw_text"],
            summary=row["summary"],
            key_points=json.loads(row["key_points"]),
            tags=json.loads(row["tags"]),
            status=row["status"],
            error_message=row["error_message"],
            char_count=row["char_count"],
            created_at=row["created_at"],
            updated_at=row["updated_at"],
            metadata=json.loads(row["metadata"]),
        )

    def add(
        self,
        title: str,
        source_type: str,
        raw_text: str = "",
        source_url: str | None = None,
        source_filename: str | None = None,
        file_path: str | None = None,
        status: str = "pending",
    ) -> KnowledgeItem:
        """Create a new knowledge item (initially pending digestion)."""
        now = datetime.now(timezone.utc).isoformat()
        item = KnowledgeItem(
            id=uuid.uuid4().hex[:12],
            title=title.strip() or "Untitled",
            source_type=source_type,
            source_url=source_url,
            source_filename=source_filename,
            file_path=file_path,
            raw_text=raw_text,
            summary="",
            key_points=[],
            tags=[],
            status=status,
            error_message=None,
            char_count=len(raw_text),
            created_at=now,
            updated_at=now,
        )
        with self._db.lock:
            self._db.conn.execute(
                """INSERT INTO knowledge_items
                   (id, title, source_type, source_url, source_filename, file_path,
                    raw_text, summary, key_points, tags, status, error_message,
                    char_count, created_at, updated_at, metadata)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                (
                    item.id, item.title, item.source_type, item.source_url,
                    item.source_filename, item.file_path, item.raw_text,
                    item.summary, json.dumps(item.key_points),
                    json.dumps(item.tags), item.status, item.error_message,
                    item.char_count, item.created_at, item.updated_at,
                    json.dumps(item.metadata, ensure_ascii=False),
                ),
            )
            self._db.conn.commit()
        self._log.info("knowledge_added", item_id=item.id, source_type=source_type)
        return item

    def update_digestion(
        self,
        item_id: str,
        summary: str,
        key_points: list[str],
        tags: list[str],
        status: str = "ready",
        error_message: str | None = None,
    ) -> KnowledgeItem:
        """Update item with digestion results."""
        now = datetime.now(timezone.utc).isoformat()
        with self._db.lock:
            self._db.conn.execute(
                """UPDATE knowledge_items
                   SET summary = ?, key_points = ?, tags = ?,
                       status = ?, error_message = ?, updated_at = ?
                   WHERE id = ?""",
                (
                    summary,
                    json.dumps(key_points, ensure_ascii=False),
                    json.dumps(tags, ensure_ascii=False),
                    status,
                    error_message,
                    now,
                    item_id,
                ),
            )
            self._db.conn.commit()
        return self.get(item_id)

    def get(self, item_id: str) -> KnowledgeItem:
        """Load a single knowledge item."""
        with self._db.lock:
            row = self._db.conn.execute(
                "SELECT * FROM knowledge_items WHERE id = ?", (item_id,)
            ).fetchone()
        if row is None:
            raise FileNotFoundError(f"Knowledge item not found: {item_id}")
        return self._row_to_item(row)

    def list_all(self, limit: int = 200) -> list[KnowledgeItem]:
        """List all items, newest first."""
        with self._db.lock:
            rows = self._db.conn.execute(
                "SELECT * FROM knowledge_items ORDER BY created_at DESC LIMIT ?",
                (limit,),
            ).fetchall()
        return [self._row_to_item(row) for row in rows]

    def list_ready(self) -> list[KnowledgeItem]:
        """List only items with status='ready'."""
        with self._db.lock:
            rows = self._db.conn.execute(
                "SELECT * FROM knowledge_items WHERE status = 'ready' "
                "ORDER BY created_at DESC"
            ).fetchall()
        return [self._row_to_item(row) for row in rows]

    def search(self, query: str, limit: int = 10) -> list[KnowledgeItem]:
        """Search knowledge by keyword across title, summary, key_points, tags, and raw_text."""
        pattern = f"%{query}%"
        with self._db.lock:
            rows = self._db.conn.execute(
                """SELECT * FROM knowledge_items
                   WHERE status = 'ready'
                     AND (title LIKE ? COLLATE NOCASE
                          OR summary LIKE ? COLLATE NOCASE
                          OR key_points LIKE ? COLLATE NOCASE
                          OR tags LIKE ? COLLATE NOCASE
                          OR raw_text LIKE ? COLLATE NOCASE)
                   ORDER BY created_at DESC
                   LIMIT ?""",
                (pattern, pattern, pattern, pattern, pattern, limit),
            ).fetchall()
        return [self._row_to_item(row) for row in rows]

    def update_title(self, item_id: str, new_title: str) -> KnowledgeItem:
        """Update the title of a knowledge item."""
        now = datetime.now(timezone.utc).isoformat()
        with self._db.lock:
            self._db.conn.execute(
                "UPDATE knowledge_items SET title = ?, updated_at = ? WHERE id = ?",
                (new_title.strip() or "Untitled", now, item_id),
            )
            self._db.conn.commit()
        return self.get(item_id)

    def delete(self, item_id: str) -> bool:
        """Delete a knowledge item."""
        with self._db.lock:
            cursor = self._db.conn.execute(
                "DELETE FROM knowledge_items WHERE id = ?", (item_id,)
            )
            self._db.conn.commit()
        deleted = cursor.rowcount > 0
        if deleted:
            self._log.info("knowledge_deleted", item_id=item_id)
        return deleted

    # ── Connection methods ──────────────────────────────────────────────

    def add_connections(self, connections: list[dict[str, Any]]) -> list[KnowledgeConnection]:
        """Bulk-insert connections. Each dict needs source_id, target_id, label, strength."""
        now = datetime.now(timezone.utc).isoformat()
        results: list[KnowledgeConnection] = []
        for c in connections:
            conn = KnowledgeConnection(
                id=uuid.uuid4().hex[:12],
                source_id=c["source_id"],
                target_id=c["target_id"],
                label=c.get("label", ""),
                strength=max(0.0, min(1.0, float(c.get("strength", 0.5)))),
                created_at=now,
            )
            results.append(conn)
        with self._db.lock:
            for conn in results:
                self._db.conn.execute(
                    """INSERT INTO knowledge_connections
                       (id, source_id, target_id, label, strength, created_at)
                       VALUES (?, ?, ?, ?, ?, ?)""",
                    (conn.id, conn.source_id, conn.target_id,
                     conn.label, conn.strength, conn.created_at),
                )
            self._db.conn.commit()
        if results:
            self._log.info("connections_added", count=len(results))
        return results

    def list_connections(self) -> list[KnowledgeConnection]:
        """Return all connections."""
        with self._db.lock:
            rows = self._db.conn.execute(
                "SELECT * FROM knowledge_connections ORDER BY created_at DESC"
            ).fetchall()
        return [
            KnowledgeConnection(
                id=r["id"],
                source_id=r["source_id"],
                target_id=r["target_id"],
                label=r["label"],
                strength=r["strength"],
                created_at=r["created_at"],
            )
            for r in rows
        ]

    def delete_connections_for(self, item_id: str) -> int:
        """Delete all connections involving an item (called on item delete)."""
        with self._db.lock:
            cursor = self._db.conn.execute(
                "DELETE FROM knowledge_connections WHERE source_id = ? OR target_id = ?",
                (item_id, item_id),
            )
            self._db.conn.commit()
        return cursor.rowcount

    def get_graph(self) -> dict[str, Any]:
        """Return the full knowledge graph (nodes + edges + entities) for the mindmap."""
        items = self.list_all()
        connections = self.list_connections()
        entities = self.list_all_entities()
        entity_connections = self.list_entity_connections()

        item_ids = {item.id for item in items}
        entity_ids = {e.id for e in entities}
        valid_connections = [
            c for c in connections
            if c.source_id in item_ids and c.target_id in item_ids
        ]
        valid_entity_connections = [
            c for c in entity_connections
            if c.source_id in entity_ids and c.target_id in entity_ids
        ]
        return {
            "nodes": [item.to_summary_dict() for item in items],
            "edges": [c.to_dict() for c in valid_connections],
            "entities": [e.to_dict() for e in entities],
            "entity_edges": [c.to_dict() for c in valid_entity_connections],
        }

    # ── Entity methods ─────────────────────────────────────────────────

    @staticmethod
    def _row_to_entity(row: Any) -> KnowledgeEntity:
        return KnowledgeEntity(
            id=row["id"],
            item_id=row["item_id"],
            entity_type=row["entity_type"],
            name=row["name"],
            description=row["description"],
            properties=json.loads(row["properties"]),
            created_at=row["created_at"],
        )

    def add_entities(self, item_id: str, entities: list[dict[str, Any]]) -> list[KnowledgeEntity]:
        """Bulk-insert entities extracted from a knowledge item."""
        now = datetime.now(timezone.utc).isoformat()
        results: list[KnowledgeEntity] = []
        for e in entities:
            etype = str(e.get("entity_type", "")).lower().strip()
            if etype not in PROCUREMENT_ENTITY_TYPES:
                continue
            name = str(e.get("name", ""))[:200].strip()
            if not name:
                continue
            entity = KnowledgeEntity(
                id=uuid.uuid4().hex[:12],
                item_id=item_id,
                entity_type=etype,
                name=name,
                description=str(e.get("description", ""))[:500],
                properties=e.get("properties", {}) if isinstance(e.get("properties"), dict) else {},
                created_at=now,
            )
            results.append(entity)
        with self._db.lock:
            for entity in results:
                self._db.conn.execute(
                    """INSERT INTO knowledge_entities
                       (id, item_id, entity_type, name, description, properties, created_at)
                       VALUES (?, ?, ?, ?, ?, ?, ?)""",
                    (entity.id, entity.item_id, entity.entity_type,
                     entity.name, entity.description,
                     json.dumps(entity.properties, ensure_ascii=False),
                     entity.created_at),
                )
            self._db.conn.commit()
        if results:
            self._log.info("entities_added", item_id=item_id, count=len(results))
        return results

    def get_entities_for_item(self, item_id: str) -> list[KnowledgeEntity]:
        """Return all entities extracted from a specific knowledge item."""
        with self._db.lock:
            rows = self._db.conn.execute(
                "SELECT * FROM knowledge_entities WHERE item_id = ? ORDER BY entity_type, name",
                (item_id,),
            ).fetchall()
        return [self._row_to_entity(r) for r in rows]

    def list_all_entities(self) -> list[KnowledgeEntity]:
        """Return all entities across all knowledge items."""
        with self._db.lock:
            rows = self._db.conn.execute(
                "SELECT * FROM knowledge_entities ORDER BY entity_type, name"
            ).fetchall()
        return [self._row_to_entity(r) for r in rows]

    def delete_entities_for(self, item_id: str) -> int:
        """Delete all entities for an item (cascades entity connections via FK)."""
        with self._db.lock:
            cursor = self._db.conn.execute(
                "DELETE FROM knowledge_entities WHERE item_id = ?", (item_id,),
            )
            self._db.conn.commit()
        return cursor.rowcount

    def add_entity_connections(self, connections: list[dict[str, Any]]) -> list[KnowledgeEntityConnection]:
        """Bulk-insert entity-to-entity connections."""
        now = datetime.now(timezone.utc).isoformat()
        results: list[KnowledgeEntityConnection] = []
        for c in connections:
            conn = KnowledgeEntityConnection(
                id=uuid.uuid4().hex[:12],
                source_id=c["source_id"],
                target_id=c["target_id"],
                label=str(c.get("label", ""))[:100],
                strength=max(0.0, min(1.0, float(c.get("strength", 0.5)))),
                created_at=now,
            )
            results.append(conn)
        with self._db.lock:
            for conn in results:
                self._db.conn.execute(
                    """INSERT INTO knowledge_entity_connections
                       (id, source_id, target_id, label, strength, created_at)
                       VALUES (?, ?, ?, ?, ?, ?)""",
                    (conn.id, conn.source_id, conn.target_id,
                     conn.label, conn.strength, conn.created_at),
                )
            self._db.conn.commit()
        if results:
            self._log.info("entity_connections_added", count=len(results))
        return results

    def list_entity_connections(self) -> list[KnowledgeEntityConnection]:
        """Return all entity-to-entity connections."""
        with self._db.lock:
            rows = self._db.conn.execute(
                "SELECT * FROM knowledge_entity_connections ORDER BY created_at DESC"
            ).fetchall()
        return [
            KnowledgeEntityConnection(
                id=r["id"], source_id=r["source_id"], target_id=r["target_id"],
                label=r["label"], strength=r["strength"], created_at=r["created_at"],
            )
            for r in rows
        ]

    def get_index_string(self) -> str:
        """Generate a compact system prompt index of available knowledge.

        Returns empty string if no ready items. Lists only titles and tags
        to keep token usage low. The agent should use ``search_knowledge``
        to retrieve full details.
        """
        items = self.list_ready()
        if not items:
            return ""

        lines = [
            "## Knowledge Base",
            f"You have access to {len(items)} knowledge base item(s). "
            "Use the search_knowledge tool to retrieve detailed information.",
            "",
        ]
        for item in items:
            tags_str = f" [{', '.join(item.tags)}]" if item.tags else ""
            lines.append(f"- {item.title}{tags_str}")

        return "\n".join(lines)
