from __future__ import annotations

import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import lancedb
import requests

from .store import LANCEDB_DIR, KnowledgeRule, ensure_knowledge_store, load_rules, now_iso


TABLE_NAME = "rules"
INDEX_META_PATH = LANCEDB_DIR / "index_meta.json"
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_BATCH_SIZE = 64


@dataclass(frozen=True)
class IndexBuildResult:
    table_path: Path
    model: str
    rules_count: int


@dataclass(frozen=True)
class SemanticSearchResult:
    id: str
    topic: str
    task_ids: str
    source: str
    text: str
    distance: float


def build_vector_index(model: str | None = None) -> IndexBuildResult:
    ensure_knowledge_store()
    storage_dir = lancedb_storage_dir()
    storage_dir.mkdir(parents=True, exist_ok=True)
    selected_model = embedding_model(model)
    rules = [rule for rule in load_rules() if rule.status == "active"]
    if not rules:
        raise RuntimeError("Brak aktywnych regul do zaindeksowania.")

    texts = [rule_search_text(rule) for rule in rules]
    vectors = embed_texts(texts, selected_model)
    rows = [rule_to_lancedb_row(rule, text, vector) for rule, text, vector in zip(rules, texts, vectors)]

    db = lancedb.connect(str(storage_dir))
    db.create_table(TABLE_NAME, data=rows, mode="overwrite")
    meta = {
        "created_at": now_iso(),
        "model": selected_model,
        "rules_count": len(rows),
        "table": TABLE_NAME,
        "storage_dir": str(storage_dir),
        "table_path": str(storage_dir / f"{TABLE_NAME}.lance"),
        "rule_ids": [rule.id for rule in rules],
    }
    INDEX_META_PATH.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
    return IndexBuildResult(
        table_path=storage_dir / f"{TABLE_NAME}.lance",
        model=selected_model,
        rules_count=len(rows),
    )


def semantic_search(query: str, limit: int = 10, model: str | None = None) -> list[SemanticSearchResult]:
    ensure_knowledge_store()
    query = query.strip()
    if not query:
        return []
    selected_model = embedding_model(model)
    storage_dir = lancedb_storage_dir()
    db = lancedb.connect(str(storage_dir))
    if TABLE_NAME not in table_names(db):
        raise RuntimeError("Brak indeksu LanceDB. Uruchom: python gads.py wiedza indeksuj")

    query_vector = embed_texts([query], selected_model)[0]
    table = db.open_table(TABLE_NAME)
    rows = table.search(query_vector).limit(max(limit, 0)).to_list()
    return [
        SemanticSearchResult(
            id=str(row.get("id") or ""),
            topic=str(row.get("topic") or ""),
            task_ids=str(row.get("task_ids") or ""),
            source=str(row.get("source") or ""),
            text=str(row.get("text") or ""),
            distance=float(row.get("_distance") or 0.0),
        )
        for row in rows
    ]


def vector_index_summary() -> dict[str, Any]:
    ensure_knowledge_store()
    if not INDEX_META_PATH.exists():
        return {}
    meta = json.loads(INDEX_META_PATH.read_text(encoding="utf-8"))
    table_path = Path(str(meta.get("table_path") or ""))
    current_table_path = lancedb_storage_dir() / f"{TABLE_NAME}.lance"
    meta["available"] = table_path.exists()
    meta["current_storage_dir"] = str(lancedb_storage_dir())
    meta["current_table_path"] = str(current_table_path)
    meta["current_available"] = current_table_path.exists()
    return meta


def lancedb_storage_dir() -> Path:
    configured = os.environ.get("KNOWLEDGE_LANCEDB_DIR")
    if configured:
        return Path(configured)
    local_app_data = os.environ.get("LOCALAPPDATA")
    if local_app_data:
        return Path(local_app_data) / "google-ads-ver2-knowledge-lancedb"
    return LANCEDB_DIR


def embedding_model(model: str | None = None) -> str:
    return (
        model
        or os.environ.get("KNOWLEDGE_EMBEDDING_MODEL")
        or DEFAULT_EMBEDDING_MODEL
    )


def embed_texts(texts: list[str], model: str) -> list[list[float]]:
    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        raise RuntimeError("Brak OPENAI_API_KEY. Dodaj klucz do .env.")

    vectors: list[list[float]] = []
    for start in range(0, len(texts), EMBEDDING_BATCH_SIZE):
        batch = texts[start : start + EMBEDDING_BATCH_SIZE]
        vectors.extend(call_embeddings_api(api_key, model, batch))
    if len(vectors) != len(texts):
        raise RuntimeError("Liczba embeddingow nie zgadza sie z liczba tekstow.")
    return vectors


def call_embeddings_api(api_key: str, model: str, texts: list[str]) -> list[list[float]]:
    base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1").rstrip("/")
    response = requests.post(
        f"{base_url}/embeddings",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        json={
            "model": model,
            "input": texts,
            "encoding_format": "float",
        },
        timeout=120,
    )
    if response.status_code >= 400:
        raise RuntimeError(f"OpenAI embeddings API zwrocilo blad {response.status_code}: {response.text}")
    payload = response.json()
    data = payload.get("data") or []
    data = sorted(data, key=lambda item: int(item.get("index", 0)))
    vectors = [item.get("embedding") for item in data]
    if not all(isinstance(vector, list) and vector for vector in vectors):
        raise RuntimeError("OpenAI embeddings API nie zwrocilo poprawnych wektorow.")
    return vectors


def rule_to_lancedb_row(rule: KnowledgeRule, search_text: str, vector: list[float]) -> dict[str, Any]:
    return {
        "id": rule.id,
        "status": rule.status,
        "topic": rule.topic,
        "task_ids": ", ".join(rule.task_ids),
        "suggested_task_ids": ", ".join(rule.suggested_task_ids),
        "rule_type": rule.rule_type,
        "condition": rule.condition,
        "recommendation": rule.recommendation,
        "risk": rule.risk,
        "source": rule.source,
        "source_file": rule.source_file,
        "confidence": rule.confidence,
        "created_at": rule.created_at,
        "updated_at": rule.updated_at,
        "duplicate_of": rule.duplicate_of,
        "supersedes": ", ".join(rule.supersedes),
        "text": rule.text,
        "search_text": search_text,
        "vector": vector,
    }


def rule_search_text(rule: KnowledgeRule) -> str:
    values = [
        f"ID: {rule.id}",
        f"Temat: {rule.topic}",
        f"Typ: {rule.rule_type}",
        f"Warunek: {rule.condition}",
        f"Rekomendacja: {rule.recommendation}",
        f"Ryzyko: {rule.risk}",
        f"Tekst: {rule.text}",
        f"Zadania: {', '.join(rule.task_ids)}",
        f"Zrodlo: {rule.source}",
    ]
    return "\n".join(value for value in values if value.strip())


def table_names(db: Any) -> list[str]:
    if hasattr(db, "table_names"):
        return list(db.table_names())
    return list(db.list_tables())
