from __future__ import annotations

import json
from datetime import date, datetime

from sqlalchemy import text

from app.modules.rag.contracts import EvidenceLink, RagDocument, RagSource, RagSpan
from app.modules.rag.contracts.enums import RagLayer


class RagCacheRepository:
    def get_cached_documents(self, repo_id: str, blob_sha: str) -> list[RagDocument]:
        with self._engine().connect() as conn:
            rows = conn.execute(text(self._select_sql()), {"repo_id": repo_id, "blob_sha": blob_sha}).mappings().fetchall()
        return [self._row_to_document(row) for row in rows]

    def cache_documents(self, repo_id: str, path: str, blob_sha: str, docs: list[RagDocument]) -> None:
        if not docs:
            return
        with self._engine().connect() as conn:
            conn.execute(
                text("DELETE FROM rag_document_cache WHERE repo_id = :repo_id AND blob_sha = :blob_sha"),
                {"repo_id": repo_id, "blob_sha": blob_sha},
            )
            for idx, doc in enumerate(docs):
                conn.execute(text(self._insert_sql()), self._insert_params(repo_id, path, blob_sha, idx, doc))
            conn.commit()

    def record_repo_cache(self, *, project_id: str, commit_sha: str | None, changed_files: list[str], summary: str) -> None:
        docs: list[RagDocument] = []
        for idx, path in enumerate(changed_files):
            docs.append(
                RagDocument(
                    layer=RagLayer.CODE_SOURCE_CHUNKS,
                    lang="python" if path.endswith(".py") else None,
                    source=RagSource(project_id, commit_sha, path),
                    title=path,
                    text=f"repo_webhook:{path}:{summary[:300]}",
                    metadata={"chunk_index": idx, "artifact_type": "CODE", "section": "repo_webhook"},
                )
            )
        for doc in docs:
            blob_sha = self._blob_sha(commit_sha, doc.source.path)
            doc.metadata["blob_sha"] = blob_sha
            self.cache_documents(project_id, doc.source.path, blob_sha, [doc])

    def _select_sql(self) -> str:
        return """
            SELECT layer, lang, path, title, content, metadata_json, links_json, span_start, span_end,
                   repo_id, commit_sha, embedding::text AS embedding_txt
            FROM rag_document_cache
            WHERE repo_id = :repo_id AND blob_sha = :blob_sha
            ORDER BY chunk_index ASC
        """

    def _insert_sql(self) -> str:
        return """
            INSERT INTO rag_document_cache (
                repo_id, blob_sha, chunk_index, path, layer, lang, title, content, embedding,
                metadata_json, links_json, span_start, span_end, commit_sha
            )
            VALUES (
                :repo_id, :blob_sha, :chunk_index, :path, :layer, :lang, :title, :content, CAST(:embedding AS vector),
                CAST(:metadata_json AS jsonb), CAST(:links_json AS jsonb), :span_start, :span_end, :commit_sha
            )
        """

    def _insert_params(self, repo_id: str, path: str, blob_sha: str, chunk_index: int, doc: RagDocument) -> dict:
        row = doc.to_record()
        metadata = self._json_ready(row["metadata"])
        emb = row["embedding"] or []
        return {
            "repo_id": repo_id,
            "blob_sha": blob_sha,
            "chunk_index": chunk_index,
            "path": path,
            "layer": row["layer"],
            "lang": row["lang"],
            "title": row["title"],
            "content": row["text"],
            "embedding": "[" + ",".join(str(x) for x in emb) + "]" if emb else None,
            "metadata_json": json.dumps(metadata, ensure_ascii=True),
            "links_json": json.dumps(row["links"], ensure_ascii=True),
            "span_start": row["span_start"],
            "span_end": row["span_end"],
            "commit_sha": row["commit_sha"],
        }

    def _row_to_document(self, row) -> RagDocument:
        metadata = self._loads(row.get("metadata_json"), default={})
        return RagDocument(
            layer=str(row["layer"]),
            lang=row.get("lang"),
            source=RagSource(repo_id=str(row["repo_id"]), commit_sha=row.get("commit_sha"), path=str(row["path"])),
            title=str(row["title"] or row["path"]),
            text=str(row["content"] or ""),
            metadata=metadata,
            links=[EvidenceLink(**item) for item in self._loads(row.get("links_json"), default=[])],
            span=RagSpan(row.get("span_start"), row.get("span_end")),
            embedding=self._parse_vector(str(row["embedding_txt"] or "")),
        )

    def _blob_sha(self, commit_sha: str | None, path: str) -> str:
        from hashlib import sha256

        return sha256(f"{commit_sha or 'no-commit'}:{path}".encode("utf-8")).hexdigest()

    def _engine(self):
        from app.modules.shared.db import get_engine

        return get_engine()

    def _json_ready(self, value):
        if isinstance(value, dict):
            return {str(key): self._json_ready(item) for key, item in value.items()}
        if isinstance(value, list):
            return [self._json_ready(item) for item in value]
        if isinstance(value, tuple):
            return [self._json_ready(item) for item in value]
        if isinstance(value, (date, datetime)):
            return value.isoformat()
        return value

    def _loads(self, value, *, default):
        if value is None:
            return default
        if isinstance(value, (dict, list)):
            return value
        text_value = str(value).strip()
        return default if not text_value else json.loads(text_value)

    def _parse_vector(self, value: str) -> list[float]:
        text_value = value.strip()
        if not text_value:
            return []
        if text_value.startswith("[") and text_value.endswith("]"):
            text_value = text_value[1:-1]
        return [float(part.strip()) for part in text_value.split(",") if part.strip()]
