from __future__ import annotations

import hashlib
import json
import re
import time
from pathlib import Path
from typing import Any

from connectors.ai_provider import assess_completeness, extract_legal_fragment, identify_product
from core_db import connect
from core_schema import init_core_schema, utc_now
from source_store import get_snapshot


def _json(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False)


def _read_snapshot_text(snapshot: dict[str, Any]) -> str:
    raw=snapshot.get("text_path")
    if not raw: raise RuntimeError("У версии нет TXT-представления.")
    path=Path(raw)
    if not path.exists(): raise RuntimeError("TXT-файл версии не найден.")
    text=path.read_text(encoding="utf-8",errors="replace").strip()
    if len(text)<200: raise RuntimeError("TXT версии слишком короткий для анализа.")
    return text


def _heading_lines(text:str)->list[str]:
    result=[]
    pattern=re.compile(r"^\s*(статья\s+\d+[\w.-]*|глава\s+\w+|раздел\s+[IVXLC\d]+|приложение\s*№?\s*\d+)\b.*$",re.I)
    for line in text.splitlines():
        line=line.strip()
        if line and pattern.match(line): result.append(line[:300])
    return result


def _local_completeness(text:str)->dict[str,Any]:
    headings=_heading_lines(text)
    article_headings={int(x) for x in re.findall(r"(?im)^\s*статья\s+(\d+)\b",text)}
    article_refs={int(x) for x in re.findall(r"(?i)стать(?:е|и|ю|я)\s+(\d+)\b",text)}
    appendix_headings={int(x) for x in re.findall(r"(?im)^\s*приложение\s*№?\s*(\d+)\b",text)}
    appendix_refs={int(x) for x in re.findall(r"(?i)приложени(?:е|я|ю|и)\s*№?\s*(\d+)\b",text)}

    missing_articles=sorted(x for x in article_refs if x not in article_headings)
    missing_appendices=sorted(x for x in appendix_refs if x not in appendix_headings)
    reasons=[]
    if missing_articles:
        reasons.append("В тексте есть ссылки на отсутствующие статьи: "+", ".join(map(str,missing_articles[:12])))
    if missing_appendices:
        reasons.append("В тексте есть ссылки на отсутствующие приложения: "+", ".join(map(str,missing_appendices[:12])))

    last=headings[-1] if headings else ""
    definite=bool(missing_articles or missing_appendices)
    return {
        "status":"incomplete" if definite else "unknown",
        "score":0.15 if definite else 0.5,
        "reasons":reasons,
        "last_visible_section":last,
        "headings":headings,
        "missing_articles":missing_articles,
        "missing_appendices":missing_appendices,
    }


def _digest(text:str,local:dict[str,Any])->str:
    headings="\n".join(local.get("headings") or [])
    return (
        "НАЧАЛО КОПИИ:\n"+text[:3200]+
        "\n\nСТРУКТУРА/ЗАГОЛОВКИ:\n"+headings[:7000]+
        "\n\nЛОКАЛЬНЫЕ ПРОВЕРКИ:\n"+
        "missing_articles="+json.dumps(local.get("missing_articles") or [])+"\n"+
        "missing_appendices="+json.dumps(local.get("missing_appendices") or [])+
        "\n\nКОНЕЦ КОПИИ:\n"+text[-5200:]
    )


def _chunks(text:str,max_chars:int=6000)->list[str]:
    paragraphs=re.split(r"\n\s*\n",text)
    chunks=[]; current=[]; size=0
    for paragraph in paragraphs:
        p=paragraph.strip()
        if not p: continue
        if len(p)>max_chars:
            if current:
                chunks.append("\n\n".join(current)); current=[]; size=0
            for i in range(0,len(p),max_chars): chunks.append(p[i:i+max_chars])
            continue
        add=len(p)+2
        if current and size+add>max_chars:
            chunks.append("\n\n".join(current)); current=[p]; size=len(p)
        else:
            current.append(p); size+=add
    if current: chunks.append("\n\n".join(current))
    return chunks or [text]


def _dedupe_rules(rules:list[dict[str,Any]])->list[dict[str,Any]]:
    seen=set(); out=[]
    for rule in rules:
        key=(rule.get("rule_type",""),re.sub(r"\s+"," ",rule.get("legal_basis","").lower()).strip(),
             re.sub(r"\s+"," ",rule.get("statement","").lower()).strip())
        if key in seen: continue
        seen.add(key); out.append(rule)
    return out


def analyze_snapshot(snapshot_id:int,*,force:bool=False,model:str|None=None)->dict[str,Any]:
    init_core_schema()
    snapshot=get_snapshot(snapshot_id)
    if not snapshot: raise RuntimeError("Версия нормативного документа не найдена.")

    with connect() as con:
        existing=con.execute("SELECT * FROM knowledge_documents WHERE source_snapshot_id=?",(snapshot_id,)).fetchone()
    if existing and not force:
        return {"ok":True,"cached":True,"knowledge_document":dict(existing)}

    text=_read_snapshot_text(snapshot)
    code=str(snapshot.get("document_code") or "")
    title=str(snapshot.get("title") or "")
    local=_local_completeness(text)

    # Definite local gaps are stronger than an AI guess and save free-tier tokens.
    ai_completeness_meta={}
    if local["status"]=="incomplete":
        completeness={
            "status":"incomplete","score":local["score"],
            "reasons":local["reasons"],"last_visible_section":local["last_visible_section"],
        }
    else:
        ai_check=assess_completeness(document_code=code,title=title,digest=_digest(text,local))
        completeness=ai_check["data"]
        ai_completeness_meta=ai_check.get("meta") or {}

    rules=[]; ai_run_ids=[]; providers=[]; models=[]
    doc_info={"code":code,"title":title,"document_type":"","revision":""}

    # If source is definitely incomplete, do not build legal rules from a broken copy.
    if completeness["status"]!="incomplete":
        chunks=_chunks(text)
        for idx,chunk in enumerate(chunks,1):
            ai=extract_legal_fragment(document_code=code,title=title,text=chunk,fragment_index=idx,fragment_total=len(chunks))
            data=ai["data"]
            if idx==1 and data.get("document"): doc_info=data["document"]
            rules.extend(data.get("rules") or [])
            ai_run_ids.append(ai.get("ai_run_id"))
            meta=ai.get("meta") or {}
            providers.append(meta.get("provider")); models.append(meta.get("model"))
            # Groq free tier has a token/minute limit. Keep chunked processing polite.
            if meta.get("provider")=="groq" and idx<len(chunks):
                time.sleep(12)
        rules=_dedupe_rules(rules)

    now=utc_now()
    status="source_incomplete" if completeness["status"]=="incomplete" else "review_required"
    provider_used=(providers[0] if providers else ai_completeness_meta.get("provider"))
    model_used=(models[0] if models else ai_completeness_meta.get("model"))

    with connect() as con:
        con.execute("UPDATE knowledge_documents SET is_current=0 WHERE document_slug=?",(snapshot["document_slug"],))
        meta_json=_json({
            "document_type":doc_info.get("document_type"),"revision":doc_info.get("revision"),
            "provider":provider_used,"ai_run_ids":[x for x in ai_run_ids if x],
            "local_completeness":{"missing_articles":local["missing_articles"],"missing_appendices":local["missing_appendices"]},
        })
        if existing:
            knowledge_id=int(existing["id"])
            con.execute("""
                UPDATE knowledge_documents SET document_code=?,title=?,source_hash=?,status=?,
                    completeness_status=?,completeness_score=?,completeness_reasons_json=?,last_visible_section=?,
                    model=?,extracted_at=?,is_current=1,meta_json=? WHERE id=?
            """,(code,doc_info.get("title") or title,snapshot["content_hash"],status,completeness["status"],
                 float(completeness["score"]),_json(completeness["reasons"]),completeness["last_visible_section"],
                 model_used,now,meta_json,knowledge_id))
            con.execute("DELETE FROM knowledge_rules WHERE knowledge_document_id=?",(knowledge_id,))
        else:
            cur=con.execute("""
                INSERT INTO knowledge_documents(document_slug,source_snapshot_id,document_code,title,source_hash,status,
                    completeness_status,completeness_score,completeness_reasons_json,last_visible_section,model,
                    extracted_at,is_current,meta_json) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,1,?)
            """,(snapshot["document_slug"],snapshot_id,code,doc_info.get("title") or title,snapshot["content_hash"],status,
                 completeness["status"],float(completeness["score"]),_json(completeness["reasons"]),
                 completeness["last_visible_section"],model_used,now,meta_json))
            knowledge_id=int(cur.lastrowid)

        for rule in rules:
            con.execute("""
                INSERT INTO knowledge_rules(knowledge_document_id,document_slug,source_snapshot_id,rule_type,legal_basis,
                    statement,conditions_json,outcomes_json,confidence,review_status,ai_run_id,created_at)
                VALUES(?,?,?,?,?,?,?,?,?,'review_required',?,?)
            """,(knowledge_id,snapshot["document_slug"],snapshot_id,rule["rule_type"],rule["legal_basis"],rule["statement"],
                 _json(rule["conditions"]),_json(rule["outcomes"]),float(rule["confidence"]),
                 ai_run_ids[0] if ai_run_ids else None,now))

        con.execute("UPDATE source_snapshots SET extraction_status=?,extraction_meta_json=? WHERE id=?",
                    ("source_incomplete" if completeness["status"]=="incomplete" else "ai_review_required",
                     _json({"knowledge_document_id":knowledge_id,"completeness_status":completeness["status"],
                            "completeness_score":completeness["score"],"rules_extracted":len(rules),"provider":provider_used}),snapshot_id))
        con.commit()

    return {"ok":True,"cached":False,"knowledge_document_id":knowledge_id,"document":doc_info,
            "completeness":completeness,"rules_extracted":len(rules),"review_status":status,
            "meta":{"provider":provider_used,"model":model_used,"chunks":len(_chunks(text)) if status!="source_incomplete" else 0}}


def list_knowledge()->list[dict[str,Any]]:
    init_core_schema()
    with connect() as con:
        rows=con.execute("""
            SELECT kd.*,
              (SELECT COUNT(*) FROM knowledge_rules kr WHERE kr.knowledge_document_id=kd.id) AS rules_count,
              (SELECT COUNT(*) FROM knowledge_rules kr WHERE kr.knowledge_document_id=kd.id AND kr.review_status='review_required') AS rules_review_required
            FROM knowledge_documents kd WHERE kd.is_current=1 ORDER BY kd.document_code
        """).fetchall()
    result=[]
    for row in rows:
        item=dict(row); item["completeness_reasons"]=json.loads(item.pop("completeness_reasons_json") or "[]")
        item["meta"]=json.loads(item.pop("meta_json") or "{}"); result.append(item)
    return result


def get_knowledge(document_slug:str)->dict[str,Any]|None:
    init_core_schema()
    with connect() as con:
        doc=con.execute("SELECT * FROM knowledge_documents WHERE document_slug=? AND is_current=1 ORDER BY id DESC LIMIT 1",(document_slug,)).fetchone()
        if not doc: return None
        rules=con.execute("SELECT * FROM knowledge_rules WHERE knowledge_document_id=? ORDER BY id",(doc["id"],)).fetchall()
    document=dict(doc); document["completeness_reasons"]=json.loads(document.pop("completeness_reasons_json") or "[]")
    document["meta"]=json.loads(document.pop("meta_json") or "{}")
    parsed=[]
    for row in rules:
        rule=dict(row); rule["conditions"]=json.loads(rule.pop("conditions_json") or "[]"); rule["outcomes"]=json.loads(rule.pop("outcomes_json") or "[]"); parsed.append(rule)
    return {"document":document,"rules":parsed}


def summary()->dict[str,Any]:
    init_core_schema()
    with connect() as con:
        docs=con.execute("SELECT COUNT(*) FROM knowledge_documents WHERE is_current=1").fetchone()[0]
        incomplete=con.execute("SELECT COUNT(*) FROM knowledge_documents WHERE is_current=1 AND completeness_status IN ('possibly_incomplete','incomplete')").fetchone()[0]
        rules=con.execute("SELECT COUNT(*) FROM knowledge_rules kr JOIN knowledge_documents kd ON kd.id=kr.knowledge_document_id WHERE kd.is_current=1").fetchone()[0]
        pending=con.execute("SELECT COUNT(*) FROM knowledge_rules kr JOIN knowledge_documents kd ON kd.id=kr.knowledge_document_id WHERE kd.is_current=1 AND kr.review_status='review_required'").fetchone()[0]
    return {"documents":int(docs),"possibly_incomplete":int(incomplete),"rules":int(rules),"review_required":int(pending)}


def product_identification(*,name:str,purpose:str,description:str,characteristics:dict[str,Any],model:str|None=None)->dict[str,Any]:
    raw=json.dumps({"name":name,"purpose":purpose,"description":description,"characteristics":characteristics},ensure_ascii=False,sort_keys=True)
    digest=hashlib.sha256(raw.encode("utf-8")).hexdigest()
    ai=identify_product(name=name,purpose=purpose,description=description,characteristics=characteristics,model=model)
    meta=ai.get("meta") or {}
    with connect() as con:
        con.execute("INSERT INTO product_analyses(created_at,input_hash,provider,model,result_json,ai_run_id) VALUES(?,?,?,?,?,?)",
                    (utc_now(),digest,str(meta.get("provider") or ""),str(meta.get("model") or model or ""),_json(ai["data"]),ai["ai_run_id"]))
        con.commit()
    return {"ok":True,"identification":ai["data"],"meta":meta}
