"""Embedder-aligned text chunking for knowledge search + graph (Phase 2)."""
from __future__ import annotations

import sqlite3
from typing import Any

_DEFAULT_CHUNK_CHARS = 1800
_DEFAULT_CHUNK_OVERLAP = 180
_CHARS_PER_TOKEN = 4
_MAX_SEQ_LEN_TOKENS = 512


def embedding_chunk_params() -> tuple[int, int]:
    """Return (size, overlap) in characters for indexing / vector chunks."""
    try:
        from core.config import get_utility_config

        cfg = get_utility_config()
        size = int(cfg.get("embedding_chunk_chars") or _DEFAULT_CHUNK_CHARS)
        overlap = int(cfg.get("embedding_chunk_overlap") or _DEFAULT_CHUNK_OVERLAP)
    except Exception:
        size = _DEFAULT_CHUNK_CHARS
        overlap = _DEFAULT_CHUNK_OVERLAP
    try:
        from utils.embedding_model import _MAX_SEQ_LEN, _SUBWINDOW_CHARS

        token_cap = int(_MAX_SEQ_LEN) * _CHARS_PER_TOKEN
        subwindow = int(_SUBWINDOW_CHARS)
        size = min(size, subwindow, token_cap)
    except Exception:
        pass
    size = max(400, min(size, 24000))
    overlap = max(0, min(overlap, size // 2))
    return size, overlap


def CHUNKER_VERSION() -> str:
    """Stable version string stored in search_meta.chunker_version."""
    size, overlap = embedding_chunk_params()
    return f"v2-embed-{size}-{overlap}"


def chunk_text_for_embedding(text: str) -> list[str]:
    """Split text into chunks sized for one embedder forward pass."""
    size, overlap = embedding_chunk_params()
    t = (text or "").strip()
    if not t:
        return []
    if len(t) <= size:
        return [t]
    chunks: list[str] = []
    start = 0
    step = max(1, size - overlap)
    while start < len(t):
        chunk = t[start : start + size].strip()
        if chunk:
            chunks.append(chunk)
        start += step
    return chunks


def get_global_chunker_version(conn: sqlite3.Connection) -> str:
    """Read search_meta.chunker_version from an open connection."""
    row = conn.execute("SELECT value FROM search_meta WHERE key = ?", ("chunker_version",)).fetchone()
    return str(row[0] or "") if row else ""


def thread_chunker_version_stale(conn: sqlite3.Connection, thread_id: str) -> bool:
    """True when a knowledge thread's chunker stamp differs from CHUNKER_VERSION()."""
    tid = (thread_id or "").strip()
    if not tid.startswith("knowledge:"):
        return False
    current = CHUNKER_VERSION()
    key = f"thread_chunker_version:{tid}"
    row = conn.execute("SELECT value FROM search_meta WHERE key = ?", (key,)).fetchone()
    stored = str(row[0] or "") if row else ""
    if stored:
        return stored != current
    # Missing per-thread stamp: stale only when an index exists (v2 migration / partial reindex).
    fts_row = conn.execute(
        "SELECT COUNT(*) FROM messages_fts WHERE thread_id = ?",
        (tid,),
    ).fetchone()
    fts_n = int(fts_row[0] or 0) if fts_row else 0
    return fts_n > 0


def chunker_version_stale(conn: sqlite3.Connection | None = None) -> bool:
    """True when the global DB chunker stamp differs from the active CHUNKER_VERSION."""
    current = CHUNKER_VERSION()
    if conn is not None:
        stored = get_global_chunker_version(conn)
        return (not stored) or stored != current
    try:
        import session_search_db as ss

        c = ss._safe_get_connection()
        if c is None:
            return True
        try:
            return chunker_version_stale(c)
        finally:
            c.close()
    except Exception:
        return True


def corpus_semantic_index_state(
    thread_id: str,
    *,
    embedding_reindex_state: str = "idle",
) -> str:
    """Read-only status for get_knowledge_status (Phase 2 API hook)."""
    try:
        from utils.embedding_model import embedding_enabled
    except Exception:
        return "fts_only"
    if not embedding_enabled():
        return "fts_only"
    if embedding_reindex_state in ("reindexing", "preparing_model"):
        return "reindexing"
    tid = (thread_id or "").strip()
    if not tid.startswith("knowledge:"):
        return "ready"
    try:
        import session_search_db as ss

        conn = ss._safe_get_connection()
        if conn is None:
            return "fts_only"
        try:
            if thread_chunker_version_stale(conn, tid):
                return "stale_chunker"
            if not ss.knowledge_semantic_index_complete(conn, tid):
                fts_row = conn.execute(
                    "SELECT COUNT(*) FROM messages_fts WHERE thread_id = ?",
                    (tid,),
                ).fetchone()
                fts_n = int(fts_row[0] or 0) if fts_row else 0
                return "vectors_missing" if fts_n > 0 else "fts_only"
            stored = ss._get_meta(conn, ss._embed_identity_meta_key(tid))
            try:
                from utils.embedding_model import embedding_identity_stale

                identity_stale = not stored or embedding_identity_stale(stored)
            except Exception:
                from utils.embedding_model import embedding_identity

                current = embedding_identity()
                identity_stale = not stored or (bool(current) and stored != current)
            if identity_stale:
                return "stale_identity"
            return "ready"
        finally:
            conn.close()
    except Exception:
        return "fts_only"
