from __future__ import annotations

import argparse
import datetime as dt
import json
import mimetypes
import re
import subprocess
import sys
import threading
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, unquote, urlparse
from urllib.request import Request, urlopen

from build_1c_agent_intake import build_intake
from common import search_lexical_index
from normalize_1c_its_docs import TextExtractor, clean_its_text, decode_html
from route_1c_question import route_question
from resolve_1c_fact import member_area, resolve_from_route_index, resolve_from_snapshot, split_fact_path


ROOT = Path(__file__).resolve().parents[1]
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8770
STATIC_DIR = ROOT / "tools" / "management-console"
REPORTS = ROOT / "reports"
JOB_LOCK = threading.Lock()
JOBS: dict[str, dict] = {}
RAG_INDEX = ROOT / "plugins" / "1c" / "datasets" / "prepared" / "rag_index.json"
DEFAULT_1C_ROUTE_INDEX = ROOT / "reports" / "1c-sql" / "upo" / "unified-object-route-index.json"
OFFICIAL_DOCS = ROOT / "plugins" / "1c" / "rag" / "official-docs"
OFFICIAL_NORMALIZED_MANIFEST = OFFICIAL_DOCS / "normalized" / "manifest.json"
OFFICIAL_STATIC_DIR = OFFICIAL_DOCS / "static"
OFFICIAL_1C_SOURCE_TYPES = [
    "official_1c_its_admin_guide",
    "official_1c_its_admin_methodical_support",
    "official_1c_its_bsp_doc",
    "official_1c_its_client_server_admin_guide",
    "official_1c_its_dev_section",
    "official_1c_its_developer_book",
    "official_1c_its_developer_guide",
    "official_1c_its_developer_methodical_support",
    "official_1c_its_development_standards",
    "official_1c_its_fresh_configuration_guide",
    "official_1c_its_fresh_developer_guide",
    "official_1c_its_glossary",
    "official_1c_its_integration_library_doc",
    "official_1c_its_methodical_support",
    "official_1c_its_platform_doc",
    "official_1c_its_taxi_user_guide",
    "official_1c_its_user_guide",
]


COMMANDS: dict[str, dict] = {
    "artifact_manifest": {
        "label": "Build artifact manifest",
        "command": [sys.executable, "scripts/build_llm_artifact_manifest.py", "--output", "reports/llm-artifact-manifest.json"],
        "timeout": 120,
        "category": "artifacts",
    },
    "artifact_check": {
        "label": "Check artifact manifest",
        "command": [sys.executable, "scripts/check_llm_artifact_manifest.py", "--manifest", "reports/llm-artifact-manifest.json", "--output", "reports/llm-artifact-check.json"],
        "timeout": 120,
        "category": "artifacts",
    },
    "model_storage": {
        "label": "Check model storage",
        "command": [sys.executable, "scripts/check_model_storage.py", "--report", "reports/model-storage.json"],
        "timeout": 120,
        "category": "models",
    },
    "q6_download_start": {
        "label": "Start Q6 1C model download",
        "command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/download_1c_q6_model.ps1", "-Action", "start"],
        "timeout": 30,
        "category": "models",
    },
    "q6_download_status": {
        "label": "Check Q6 1C model download",
        "command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/download_1c_q6_model.ps1", "-Action", "status"],
        "timeout": 30,
        "category": "models",
    },
    "q6_download_stop": {
        "label": "Stop Q6 1C model download",
        "command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/download_1c_q6_model.ps1", "-Action", "stop"],
        "timeout": 30,
        "category": "models",
    },
    "q6_service_start": {
        "label": "Start Q6 1C model service",
        "command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/manage_gpu_q6_service.ps1", "-Action", "start"],
        "timeout": 60,
        "category": "models",
    },
    "q6_service_stop": {
        "label": "Stop Q6 1C model service",
        "command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/manage_gpu_q6_service.ps1", "-Action", "stop"],
        "timeout": 60,
        "category": "models",
    },
    "q6_service_restart": {
        "label": "Restart Q6 1C model service",
        "command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/manage_gpu_q6_service.ps1", "-Action", "restart"],
        "timeout": 90,
        "category": "models",
    },
    "q6_service_status": {
        "label": "Check Q6 1C model service",
        "command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/manage_gpu_q6_service.ps1", "-Action", "status"],
        "timeout": 60,
        "category": "models",
    },
    "1c_plugin": {
        "label": "Check 1C plugin",
        "command": [sys.executable, "scripts/check_1c_plugin.py", "--report", "reports/1c-plugin-health.json"],
        "timeout": 180,
        "category": "1c",
    },
    "1c_question_router": {
        "label": "Check 1C question router",
        "command": [sys.executable, "scripts/check_1c_question_router.py", "--output", "reports/1c-question-router.json", "--print"],
        "timeout": 120,
        "category": "1c",
    },
    "1c_agent_intake": {
        "label": "Check 1C agent intake",
        "command": [sys.executable, "scripts/check_1c_agent_intake.py", "--output", "reports/1c-agent-intake.json", "--print"],
        "timeout": 120,
        "category": "1c",
    },
    "1c_rag_freshness": {
        "label": "Check 1C RAG freshness",
        "command": [sys.executable, "scripts/check_1c_rag_freshness.py", "--print"],
        "timeout": 60,
        "category": "rag",
    },
    "1c_rag_source_governance": {
        "label": "Check 1C RAG source governance",
        "command": [sys.executable, "scripts/check_1c_rag_source_governance.py", "--output", "reports/1c-rag-source-governance.json", "--print"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_private": {
        "label": "Check official docs private artifacts",
        "command": [sys.executable, "scripts/check_1c_official_docs_private_artifacts.py", "--output", "reports/1c-official-docs-private-check.json"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_access": {
        "label": "Check 1C:ITS protected access",
        "command": ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.Security; $p='plugins/1c/rag/official-docs/.local/its-cookie.dpapi.txt'; $b=[Convert]::FromBase64String((Get-Content -LiteralPath $p -Raw -Encoding UTF8).Trim()); $plain=[System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser); $env:ONEC_ITS_COOKIE=[System.Text.Encoding]::UTF8.GetString($plain); python -X utf8 scripts/check_1c_its_access.py --print"],
        "timeout": 120,
        "category": "rag",
    },
    "official_docs_cookie_normalizer": {
        "label": "Check 1C:ITS cookie normalizer",
        "command": [sys.executable, "scripts/check_1c_its_cookie_normalizer.py", "--print"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_quality": {
        "label": "Check official docs quality",
        "command": [sys.executable, "scripts/check_1c_official_docs_quality.py", "--output", "reports/1c-official-docs-quality.json", "--print"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_ingestion_logic": {
        "label": "Check official docs ingestion logic",
        "command": [sys.executable, "scripts/check_1c_its_ingestion_logic.py"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_fetch_plan": {
        "label": "Inspect official docs fetch plan",
        "command": [sys.executable, "scripts/inspect_1c_its_fetch_plan.py", "--output", "reports/1c-official-docs-fetch-plan.json", "--print"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_discover_starts": {
        "label": "Discover official docs start links",
        "command": ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.Security; $p='plugins/1c/rag/official-docs/.local/its-cookie.dpapi.txt'; $b=[Convert]::FromBase64String((Get-Content -LiteralPath $p -Raw -Encoding UTF8).Trim()); $plain=[System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser); $env:ONEC_ITS_COOKIE=[System.Text.Encoding]::UTF8.GetString($plain); python -X utf8 scripts/discover_1c_its_start_links.py --output plugins/1c/rag/official-docs/start-links.json --print"],
        "timeout": 120,
        "category": "rag",
    },
    "official_docs_start_coverage": {
        "label": "Check official docs start coverage",
        "command": [sys.executable, "scripts/check_1c_its_start_link_coverage.py", "--output", "reports/1c-official-docs-start-coverage.json", "--print"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_platform_versions": {
        "label": "Build official docs platform version catalog",
        "command": [sys.executable, "scripts/build_1c_its_platform_version_catalog.py", "--print"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_download_media": {
        "label": "Download official docs media",
        "command": [sys.executable, "scripts/download_1c_its_media.py", "--output-manifest", "plugins/1c/rag/official-docs/media/manifest.json"],
        "timeout": 600,
        "category": "rag",
    },
    "official_docs_static_site": {
        "label": "Build official docs static site",
        "command": [sys.executable, "scripts/build_1c_its_static_site.py"],
        "timeout": 180,
        "category": "rag",
    },
    "official_docs_static_quality": {
        "label": "Check official docs static site",
        "command": [sys.executable, "scripts/check_1c_its_static_site.py", "--output", "reports/1c-official-docs-static-check.json", "--print"],
        "timeout": 60,
        "category": "rag",
    },
    "official_docs_reload_fresh": {
        "label": "Reload official docs without resume",
        "command": ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run_1c_its_docs_pipeline.ps1", "-MaxPages", "2000", "-NoResume"],
        "timeout": 7200,
        "category": "rag",
    },
    "powershell_scripts": {
        "label": "Validate PowerShell scripts",
        "command": [sys.executable, "scripts/check_powershell_scripts.py"],
        "timeout": 120,
        "category": "repo",
    },
    "its_cookie_prompt": {
        "label": "Open 1C:ITS cookie dialog",
        "command": ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/set_1c_its_cookie.ps1"],
        "timeout": 600,
        "category": "settings",
    },
}


def now_iso() -> str:
    return dt.datetime.now(dt.UTC).isoformat()


def load_json(path: Path) -> dict:
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8-sig"))
    except (json.JSONDecodeError, OSError):
        return {}


def file_info(path: Path) -> dict:
    if not path.exists():
        return {"path": str(path), "exists": False}
    stat = path.stat()
    return {
        "path": str(path),
        "exists": True,
        "size_bytes": stat.st_size,
        "mtime": dt.datetime.fromtimestamp(stat.st_mtime, dt.UTC).isoformat(),
    }


def compact_artifact_manifest() -> dict:
    manifest = load_json(REPORTS / "llm-artifact-manifest.json")
    artifacts = []
    for item in manifest.get("artifacts") or []:
        artifacts.append(
            {
                "name": item.get("name"),
                "exists": item.get("exists"),
                "files": item.get("file_count"),
                "size_bytes": item.get("total_size_bytes"),
                "secret_like_files": len(item.get("secret_like_files") or []),
                "path": item.get("path"),
            }
        )
    return {
        "created_at": manifest.get("created_at"),
        "counts": manifest.get("counts") or {},
        "artifacts": artifacts,
        "file": file_info(REPORTS / "llm-artifact-manifest.json"),
    }


def compact_model_storage() -> dict:
    report = load_json(REPORTS / "model-storage.json")
    q6_download = load_json(REPORTS / "qwen3-coder-q6-download-status.json")
    models = []
    for item in report.get("models") or []:
        models.append(
            {
                "id": item.get("id"),
                "type": item.get("type"),
                "runtime": item.get("runtime"),
                "quantization": item.get("quantization"),
                "served_model_name": item.get("served_model_name"),
                "filename": item.get("filename"),
                "status": item.get("status"),
                "reason": item.get("reason"),
                "required": item.get("required"),
                "storage_path": item.get("storage_path"),
            }
        )
    return {
        "status": report.get("status"),
        "counts": report.get("counts") or {},
        "models": models,
        "q6_download": q6_download,
        "q6_service": q6_service_status(),
        "file": file_info(REPORTS / "model-storage.json"),
    }


def q6_service_status() -> dict:
    base_url = "http://docker-gpu.cin.su:8081"
    expected_model = "qwen3-coder-1c-q6"
    status = {
        "state": "unknown",
        "base_url": base_url,
        "openai_base_url": f"{base_url}/v1",
        "openai_api_key_hint": "dummy",
        "expected_model": expected_model,
        "models": [],
        "available": False,
        "container": "llm-llama-qwen3-coder-q6-test",
        "container_status": "",
        "error": "",
    }
    try:
        result = subprocess.run(
            [
                "docker",
                "--host",
                "ssh://docker-gpu",
                "ps",
                "-a",
                "--filter",
                "name=^llm-llama-qwen3-coder-q6-test$",
                "--format",
                "{{.Status}}",
            ],
            cwd=ROOT,
            capture_output=True,
            text=True,
            timeout=8,
            check=False,
        )
        status["container_status"] = result.stdout.strip()
    except (OSError, subprocess.TimeoutExpired) as exc:
        status["container_status"] = f"docker status error: {exc}"
    try:
        request = Request(f"{base_url}/v1/models", method="GET")
        with urlopen(request, timeout=5) as response:
            payload = json.loads(response.read().decode("utf-8"))
        models = []
        for item in payload.get("data") or payload.get("models") or []:
            if isinstance(item, str):
                models.append(item)
            elif isinstance(item, dict):
                models.append(str(item.get("id") or item.get("model") or item.get("name") or ""))
        status["models"] = [model for model in models if model]
        status["available"] = expected_model in status["models"]
        status["state"] = "ok" if status["available"] else "warning"
    except Exception as exc:  # pragma: no cover - depends on live GPU host
        status["state"] = "error"
        status["error"] = str(exc)
    return status


def compact_1c_health() -> dict:
    report = load_json(REPORTS / "1c-plugin-health.json")
    return {
        "status": report.get("status"),
        "failed_checks": report.get("failed_checks") or [],
        "manifest": ((report.get("checks") or {}).get("manifest") or {}),
        "file": file_info(REPORTS / "1c-plugin-health.json"),
    }


def compact_rag() -> dict:
    prepared = ROOT / "plugins" / "1c" / "datasets" / "prepared"
    official = ROOT / "plugins" / "1c" / "rag" / "official-docs"
    private_check = load_json(REPORTS / "1c-official-docs-private-check.json")
    quality_check = load_json(REPORTS / "1c-official-docs-quality.json")
    access_check = load_json(REPORTS / "1c-its-access-check.json")
    static_check = load_json(REPORTS / "1c-official-docs-static-check.json")
    start_coverage = load_json(REPORTS / "1c-official-docs-start-coverage.json")
    platform_versions = load_json(official / "platform-versions.json")
    fetch_progress = load_json(official / "raw" / "progress.json")
    fetch_manifest = load_json(official / "raw" / "manifest.json")
    return {
        "corpus": file_info(prepared / "rag_corpus.jsonl"),
        "index": file_info(prepared / "rag_index.json"),
        "manifest": file_info(prepared / "rag_manifest.json"),
        "official_raw": file_info(official / "raw" / "manifest.json"),
        "official_start_links": file_info(official / "start-links.json"),
        "official_platform_versions": {
            "counts": platform_versions.get("counts") or {},
            "defaults": platform_versions.get("defaults") or {},
            "file": file_info(official / "platform-versions.json"),
        },
        "official_start_coverage": {
            "passed": start_coverage.get("passed"),
            "counts": start_coverage.get("counts") or {},
            "by_category": start_coverage.get("by_category") or {},
            "file": file_info(REPORTS / "1c-official-docs-start-coverage.json"),
        },
        "official_progress": {
            "status": fetch_progress.get("status"),
            "page_count": fetch_progress.get("page_count") or fetch_manifest.get("page_count"),
            "error_count": fetch_progress.get("error_count") or fetch_manifest.get("error_count"),
            "seen_count": fetch_progress.get("seen_count"),
            "queue_size": fetch_progress.get("queue_size"),
            "current_url": fetch_progress.get("current_url"),
            "resumed_pages": fetch_progress.get("resumed_pages") or ((fetch_manifest.get("resume") or {}).get("resumable_pages")),
            "skipped_existing": fetch_progress.get("skipped_existing") or ((fetch_manifest.get("resume") or {}).get("skipped_existing")),
            "updated_at_unix": fetch_progress.get("updated_at_unix"),
            "file": file_info(official / "raw" / "progress.json"),
        },
        "official_normalized": file_info(official / "normalized" / "manifest.json"),
        "official_media": file_info(official / "media" / "manifest.json"),
        "official_static": file_info(official / "static" / "manifest.json"),
        "official_static_url": "/official-docs-static/index.html",
        "official_static_check": {
            "passed": static_check.get("passed"),
            "counts": static_check.get("counts") or {},
            "file": file_info(REPORTS / "1c-official-docs-static-check.json"),
        },
        "official_private_check": {
            "passed": private_check.get("passed"),
            "counts": private_check.get("counts") or {},
            "file": file_info(REPORTS / "1c-official-docs-private-check.json"),
        },
        "official_access_check": {
            "status": access_check.get("status"),
            "findings": access_check.get("findings") or [],
            "page": access_check.get("page") or {},
            "src": access_check.get("src") or {},
            "file": file_info(REPORTS / "1c-its-access-check.json"),
        },
        "official_quality": {
            "passed": quality_check.get("passed"),
            "counts": quality_check.get("counts") or {},
            "file": file_info(REPORTS / "1c-official-docs-quality.json"),
        },
    }


def overview() -> dict:
    with JOB_LOCK:
        jobs = sorted(JOBS.values(), key=lambda item: item.get("created_at") or "", reverse=True)[:20]
    return {
        "schema": "management_console_overview.v1",
        "generated_at": now_iso(),
        "workspace": str(ROOT),
        "commands": [
            {"id": key, "label": value["label"], "category": value["category"]}
            for key, value in COMMANDS.items()
        ],
        "artifacts": compact_artifact_manifest(),
        "models": compact_model_storage(),
        "rag": compact_rag(),
        "one_c": compact_1c_health(),
        "reports": {
            "artifact_manifest": file_info(REPORTS / "llm-artifact-manifest.json"),
            "artifact_check": file_info(REPORTS / "llm-artifact-check.json"),
            "model_chat_status": file_info(REPORTS / "model-chat" / "status.md"),
        },
        "jobs": jobs,
    }


def primary_doc_text(doc: dict) -> str:
    title = str(doc.get("title") or "").strip()
    content = str(doc.get("content") or "").strip()
    if not content:
        return ""
    metadata = doc.get("metadata") or {}
    source_type = str(metadata.get("source_type") or "")
    chunk_index = int(doc.get("chunk_index") or 0)
    if source_type == "official_1c_its_glossary" and chunk_index > 0:
        return ""
    lines = content.splitlines()
    heading_candidates = [title]
    heading_candidates.extend(str(item) for item in (metadata.get("headings") or []) if item)
    if source_type == "official_1c_its_glossary":
        heading_candidates.sort(key=lambda value: (":: Глоссарий" not in value, value == title))
    for candidate in heading_candidates:
        if not candidate:
            continue
        exact_heading = f"# {candidate}"
        heading_indexes = [index for index, line in enumerate(lines) if line.strip() == exact_heading]
        if heading_indexes:
            lines = lines[heading_indexes[0] + 1 :]
            break

    trimmed_lines = []
    for line in lines:
        if line.strip() == "Назад":
            break
        trimmed_lines.append(line)

    clean_lines = []
    for line in trimmed_lines:
        normalized = line.strip()
        if not normalized or normalized in {"-", title}:
            continue
        clean_lines.append(normalized)
    result = "\n".join(clean_lines).strip() or title
    glossary_noise = (
        "Глоссарий разработчика",
        "Информационная система 1С:ИТС",
        "Мы используем файлы cookie",
        "Результаты поиска",
    )
    if source_type == "official_1c_its_glossary" and any(marker in result for marker in glossary_noise):
        return title
    return result


def rag_snippet(doc: dict, max_chars: int = 1200) -> str:
    primary = primary_doc_text(doc)
    if primary:
        return primary[:max_chars]
    metadata = doc.get("metadata") or {}
    if str(metadata.get("source_type") or "") == "official_1c_its_glossary":
        return ""
    return str(doc.get("content") or "").strip()[:max_chars]


def is_toc_like_text(text: str) -> bool:
    lines = [line.strip() for line in text.splitlines() if line.strip()]
    if not lines:
        return False
    list_like = 0
    prose_like = 0
    for line in lines:
        plain = line.lstrip("-*# ").strip()
        if not plain or plain in {"Документ", "Назад"}:
            continue
        if plain.startswith("Платформа 1С:Предприятие"):
            continue
        if re.match(r"^\d+(?:\.\d+)+\.\s+\S+", plain) or re.match(r"^Глава\s+\d+\.", plain):
            list_like += 1
            continue
        if len(plain) >= 70 and not plain.endswith(":"):
            prose_like += 1
    return list_like >= 6 and prose_like <= max(2, list_like // 5)


def doc_content_kind(doc: dict) -> str:
    metadata = doc.get("metadata") or {}
    if str(metadata.get("access_blocked") or "").casefold() == "true":
        return "access_blocked"
    text = primary_doc_text(doc)
    meaningful = [
        line.strip().lstrip("-*# ").strip()
        for line in text.splitlines()
        if line.strip()
    ]
    meaningful = [
        line
        for line in meaningful
        if line and line not in {"Документ", "Назад"} and not line.startswith("Платформа 1С:Предприятие")
    ]
    if not meaningful:
        return "metadata_only"
    if is_toc_like_text(text):
        return "toc_like"
    return "content"


def display_doc_title(doc: dict) -> str:
    metadata = doc.get("metadata") or {}
    source_type = str(metadata.get("source_type") or "")
    headings = [str(item) for item in (metadata.get("headings") or []) if item]
    if source_type == "official_1c_its_glossary":
        for heading in headings:
            if ":: Глоссарий" in heading:
                return heading
    return str(doc.get("title") or (headings[0] if headings else ""))


def rag_answer_from_results(question: str, results: list[dict]) -> str:
    if not results:
        return "Контекст по вопросу в RAG не найден."
    content_kinds = [doc_content_kind(item["document"]) for item in results[:5]]
    if content_kinds and all(kind in {"access_blocked", "toc_like", "metadata_only"} for kind in content_kinds):
        if "access_blocked" in content_kinds:
            return (
                "Найдена страница официальной документации, но тело документа закрыто текущим cookie/access. "
                "Обновите cookie ИТС и проверьте доступ; пока можно видеть только оболочку или оглавление."
            )
        return (
            "Найдена страница-оглавление официальной документации, а не полный текст раздела. "
            "Для уверенного ответа нужно загрузить конкретный подраздел из оглавления или получить тело документа через доступный hdoc/src-канал."
        )
    question_lower = question.casefold()

    exact_titles = {
        "форма": {"форма", "общая форма", "элемент формы"},
    }
    for keyword, titles in exact_titles.items():
        if keyword in question_lower:
            exact_matches = [
                item
                for item in results
                if str(item["document"].get("title") or "").strip().casefold() in titles
            ]
            if not exact_matches:
                return (
                    "Найденный контекст не содержит точной страницы термина. "
                    "Ниже показаны ближайшие источники, но их недостаточно для уверенного ответа."
                )
            exact_text = "\n\n".join(primary_doc_text(item["document"])[:1400] for item in exact_matches[:2]).casefold()
            definition_markers = ("предназнач", "объект", "содержит", "используется", "является", "отображ")
            if any(marker in exact_text for marker in definition_markers):
                return (
                    "Найдена точная страница термина в официальной документации. "
                    "Проверьте фрагменты источников ниже: краткий ответ пока строится извлечением."
                )
            return (
                "Найдена точная страница термина в официальной документации, "
                "но в загруженном фрагменте нет развернутого определения. Ниже показан источник."
            )

    top_text = "\n\n".join(primary_doc_text(item["document"])[:1400] for item in results[:3])
    lowered = top_text.casefold()
    if any(marker in lowered for marker in ("предназнач", "объект", "содержит", "используется", "является", "отображ")):
        return "Контекст найден. Проверьте фрагменты источников ниже: краткий ответ пока строится извлечением."
    return "Найденный контекст не содержит достаточного определения для уверенного ответа. Ниже показаны источники, которые были найдены."


def query_rag(payload: dict) -> dict:
    question = str(payload.get("question") or "").strip()
    if not question:
        raise ValueError("question is required")
    if "source_type" in payload:
        source_type = str(payload.get("source_type") or "").strip()
    else:
        source_type = "official_1c_its_glossary"
    if source_type in {"", "official_1c_docs"}:
        source_types = OFFICIAL_1C_SOURCE_TYPES
        source_scope = "official_1c_docs"
    elif source_type == "all_with_examples":
        source_types = None
        source_scope = "all_with_examples"
    else:
        source_types = [source_type]
        source_scope = source_type
    limit = max(1, min(int(payload.get("limit") or 5), 12))
    metadata_filters = {
        "platform_version": str(payload.get("platform_version") or "").strip(),
        "platform_doc_id": str(payload.get("platform_doc_id") or "").strip(),
    }
    index = load_json(RAG_INDEX)
    if not index:
        return {
            "question": question,
            "answer": "RAG index is missing or empty. Rebuild the corpus and index first.",
            "results": [],
        }
    results = search_lexical_index(
        index,
        question,
        limit=limit,
        candidate_limit=max(limit * 5, 20),
        dedupe_by_document=False,
        min_score=0.0,
        source_types=source_types,
        metadata_filters=metadata_filters,
    )
    compact = []
    for item in results:
        doc = item["document"]
        metadata = doc.get("metadata") or {}
        snippet = rag_snippet(doc)
        if not snippet:
            continue
        content_kind = doc_content_kind(doc)
        compact.append(
            {
                "score": round(float(item.get("score") or 0), 4),
                "source_path": doc.get("source_path"),
                "title": display_doc_title(doc),
                "chunk_index": doc.get("chunk_index"),
                "source_type": metadata.get("source_type"),
                "url": metadata.get("url"),
                "platform_version": metadata.get("platform_version"),
                "platform_doc_id": metadata.get("platform_doc_id"),
                "doc_book": metadata.get("doc_book"),
                "doc_coordinate": metadata.get("doc_coordinate"),
                "access_blocked": metadata.get("access_blocked"),
                "access_findings": metadata.get("access_findings"),
                "content_kind": content_kind,
                "snippet": snippet,
            }
        )
    answer = rag_answer_from_results(question, [item for item in results if rag_snippet(item["document"])])
    if source_scope in {"metadata", "all_with_examples"}:
        answer = (
            "Внимание: в выбранный контур входят примеры или снимки, они не подтверждают текущую базу. "
            + answer
        )
    return {
        "question": question,
        "source_type": source_type,
        "source_scope": source_scope,
        "metadata_filters": {key: value for key, value in metadata_filters.items() if value},
        "answer": answer,
        "result_count": len(compact),
        "results": compact,
    }


def resolve_workspace_path(value: str) -> Path:
    if not value:
        raise ValueError("source_path is required")
    path = Path(value)
    if not path.is_absolute():
        path = ROOT / path
    return path.resolve()


def query_1c_fact(payload: dict) -> dict:
    source_kind = str(payload.get("source_kind") or "route_index").strip()
    source_path = str(payload.get("source_path") or "").strip()
    if not source_path and DEFAULT_1C_ROUTE_INDEX.exists() and source_kind == "route_index":
        source_path = str(DEFAULT_1C_ROUTE_INDEX)
    path = resolve_workspace_path(source_path)
    if not path.is_file():
        raise ValueError(f"source_path does not exist: {path}")

    fact_path = str(payload.get("path") or "").strip()
    if fact_path:
        path_kind, path_name, path_section, path_member = split_fact_path(fact_path)
        kind = str(payload.get("kind") or path_kind or "").strip() or None
        name = str(payload.get("name") or path_name or "").strip()
        table_section = str(payload.get("table_section") or path_section or "").strip() or None
        member = str(payload.get("member") or path_member or "").strip() or None
    else:
        kind = str(payload.get("kind") or "").strip() or None
        name = str(payload.get("name") or "").strip()
        table_section = str(payload.get("table_section") or "").strip() or None
        member = str(payload.get("member") or "").strip() or None
    if not name:
        raise ValueError("Use path or name")

    common = {
        "kind": kind,
        "object_name": name,
        "member": member,
        "area": member_area(str(payload.get("area") or "any").strip() or "any"),
        "table_section": table_section,
        "view": str(payload.get("view") or "effective").strip() or "effective",
        "extension": str(payload.get("extension") or "").strip() or None,
    }
    if source_kind == "route_index":
        return resolve_from_route_index(load_json(path), index_path=path, **common)
    if source_kind == "metadata_snapshot":
        return resolve_from_snapshot(load_json(path), snapshot_path=path, **common)
    raise ValueError(f"Unsupported source_kind: {source_kind}")


def query_1c_route(payload: dict) -> dict:
    question = str(payload.get("question") or "").strip()
    if not question:
        raise ValueError("question is required")
    source_path = str(payload.get("source_path") or "").strip()
    index_path = resolve_workspace_path(source_path) if source_path else DEFAULT_1C_ROUTE_INDEX
    view = str(payload.get("view") or "effective").strip() or "effective"
    return route_question(question, index_path=index_path, view=view)


def query_1c_intake(payload: dict) -> dict:
    question = str(payload.get("question") or "").strip()
    if not question:
        raise ValueError("question is required")
    source_path = str(payload.get("source_path") or "").strip()
    index_path = resolve_workspace_path(source_path) if source_path else DEFAULT_1C_ROUTE_INDEX
    view = str(payload.get("view") or "effective").strip() or "effective"
    return build_intake(question, index=index_path, view=view)


def official_doc_row(item: dict, status_name: str, index: int) -> dict:
    quality = item.get("quality") or {}
    return {
        "index": index,
        "status": status_name,
        "source_id": item.get("source_id"),
        "source_type": item.get("source_type"),
        "title": item.get("title"),
        "url": item.get("url"),
        "raw_file": item.get("raw_file"),
        "normalized_file": item.get("normalized_file"),
        "skip_reason": item.get("skip_reason"),
        "chars": item.get("chars") or quality.get("chars"),
        "quality": {
            "content_lines": quality.get("content_lines"),
            "prose_lines": quality.get("prose_lines"),
            "word_count": quality.get("word_count"),
            "is_content": quality.get("is_content"),
        },
    }


def official_docs_pages(params: dict[str, list[str]]) -> dict:
    manifest = load_json(OFFICIAL_NORMALIZED_MANIFEST)
    rows = []
    for index, item in enumerate(manifest.get("pages") or []):
        rows.append(official_doc_row(item, "passed", index))
    for index, item in enumerate(manifest.get("skipped_pages") or []):
        rows.append(official_doc_row(item, "skipped", index))

    status_filter = (params.get("status") or ["all"])[0]
    source_filter = (params.get("source_type") or ["all"])[0]
    query = (params.get("q") or [""])[0].casefold().strip()
    if status_filter != "all":
        rows = [row for row in rows if row["status"] == status_filter]
    if source_filter != "all":
        rows = [row for row in rows if row["source_type"] == source_filter]
    if query:
        rows = [
            row
            for row in rows
            if query in str(row.get("title") or "").casefold()
            or query in str(row.get("url") or "").casefold()
            or query in str(row.get("skip_reason") or "").casefold()
        ]

    limit = max(1, min(int((params.get("limit") or ["200"])[0]), 1000))
    source_types = sorted({str(row.get("source_type") or "") for row in rows if row.get("source_type")})
    return {
        "schema": "onec_official_docs_pages.v1",
        "manifest": str(OFFICIAL_NORMALIZED_MANIFEST),
        "counts": {
            "passed": len(manifest.get("pages") or []),
            "skipped": len(manifest.get("skipped_pages") or []),
            "filtered": len(rows),
        },
        "source_types": source_types,
        "rows": rows[:limit],
    }


def safe_child(parent: Path, filename: str) -> Path:
    if not filename or "/" in filename or "\\" in filename:
        raise ValueError("Invalid file name")
    path = (parent / filename).resolve()
    parent_resolved = parent.resolve()
    if not str(path).casefold().startswith(str(parent_resolved).casefold()):
        raise ValueError("Path is outside allowed directory")
    return path


def official_docs_page_text(params: dict[str, list[str]]) -> dict:
    normalized_file = (params.get("normalized_file") or [""])[0]
    raw_file = (params.get("raw_file") or [""])[0]
    title = (params.get("title") or [""])[0]
    source_type = (params.get("source_type") or [""])[0]

    if normalized_file:
        path = safe_child(OFFICIAL_DOCS / "normalized", normalized_file)
        if not path.exists():
            raise ValueError(f"Normalized file does not exist: {normalized_file}")
        text = path.read_text(encoding="utf-8-sig", errors="ignore")
        return {"schema": "onec_official_docs_page_text.v1", "kind": "normalized", "path": str(path), "text": text[:20000]}

    if not raw_file:
        raise ValueError("raw_file or normalized_file is required")
    path = safe_child(OFFICIAL_DOCS / "raw", raw_file)
    if not path.exists():
        raise ValueError(f"Raw file does not exist: {raw_file}")
    raw_bytes = path.read_bytes()
    record = {"content_type": "text/html; charset=Windows-1251"}
    raw_text = decode_html(raw_bytes, record)
    extractor = TextExtractor()
    extractor.feed(raw_text)
    extracted = extractor.text()
    cleaned = clean_its_text(extracted, title or path.stem, source_type)
    return {
        "schema": "onec_official_docs_page_text.v1",
        "kind": "raw_cleaned_preview",
        "path": str(path),
        "text": (cleaned or extracted)[:20000],
    }


def run_job(job_id: str, command_id: str) -> None:
    spec = COMMANDS[command_id]
    started = now_iso()
    with JOB_LOCK:
        JOBS[job_id].update({"status": "running", "started_at": started})
    try:
        result = subprocess.run(
            spec["command"],
            cwd=ROOT,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=int(spec.get("timeout") or 120),
            check=False,
        )
        status = "ok" if result.returncode == 0 else "failed"
        payload = {
            "status": status,
            "finished_at": now_iso(),
            "returncode": result.returncode,
            "stdout": result.stdout[-12000:],
            "stderr": result.stderr[-12000:],
        }
    except subprocess.TimeoutExpired as exc:
        payload = {
            "status": "timeout",
            "finished_at": now_iso(),
            "returncode": None,
            "stdout": (exc.stdout or "")[-12000:] if isinstance(exc.stdout, str) else "",
            "stderr": (exc.stderr or "")[-12000:] if isinstance(exc.stderr, str) else "",
        }
    except Exception as exc:  # noqa: BLE001 - local console must report all failures.
        payload = {
            "status": "failed",
            "finished_at": now_iso(),
            "returncode": None,
            "stdout": "",
            "stderr": str(exc),
        }
    with JOB_LOCK:
        JOBS[job_id].update(payload)


def run_custom_job(job_id: str, command: list[str]) -> None:
    started = now_iso()
    with JOB_LOCK:
        JOBS[job_id].update({"status": "running", "started_at": started})
    try:
        result = subprocess.run(
            command,
            cwd=ROOT,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=1200,
            check=False,
        )
        status = "ok" if result.returncode == 0 else "failed"
        payload = {
            "status": status,
            "finished_at": now_iso(),
            "returncode": result.returncode,
            "stdout": result.stdout[-12000:],
            "stderr": result.stderr[-12000:],
        }
    except subprocess.TimeoutExpired as exc:
        payload = {
            "status": "timeout",
            "finished_at": now_iso(),
            "returncode": None,
            "stdout": (exc.stdout or "")[-12000:] if isinstance(exc.stdout, str) else "",
            "stderr": (exc.stderr or "")[-12000:] if isinstance(exc.stderr, str) else "",
        }
    except Exception as exc:  # noqa: BLE001
        payload = {"status": "failed", "finished_at": now_iso(), "returncode": None, "stdout": "", "stderr": str(exc)}
    with JOB_LOCK:
        JOBS[job_id].update(payload)


def start_job(command_id: str) -> dict:
    if command_id not in COMMANDS:
        raise ValueError(f"Unknown command: {command_id}")
    job_id = str(uuid.uuid4())
    spec = COMMANDS[command_id]
    job = {
        "id": job_id,
        "command_id": command_id,
        "label": spec["label"],
        "category": spec["category"],
        "command": spec["command"],
        "status": "queued",
        "created_at": now_iso(),
    }
    with JOB_LOCK:
        JOBS[job_id] = job
    thread = threading.Thread(target=run_job, args=(job_id, command_id), daemon=True)
    thread.start()
    return job


def start_target_its_job(url: str, *, max_pages: int = 12, max_depth: int = 2) -> dict:
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"} or parsed.netloc.lower() != "its.1c.ru":
        raise ValueError("Only https://its.1c.ru/... URLs are allowed")
    if "/db/" not in parsed.path:
        raise ValueError("Only 1C:ITS /db/... documentation URLs are allowed")
    max_pages = max(1, min(max_pages, 50))
    max_depth = max(0, min(max_depth, 4))
    command = [
        "powershell",
        "-NoProfile",
        "-ExecutionPolicy",
        "Bypass",
        "-File",
        "scripts/run_1c_its_target_doc_pipeline.ps1",
        "-Url",
        url,
        "-MaxPages",
        str(max_pages),
        "-MaxDepth",
        str(max_depth),
    ]
    job_id = str(uuid.uuid4())
    job = {
        "id": job_id,
        "command_id": "official_docs_fetch_target",
        "label": "Fetch target 1C:ITS document",
        "category": "rag",
        "command": command,
        "status": "queued",
        "created_at": now_iso(),
    }
    with JOB_LOCK:
        JOBS[job_id] = job
    thread = threading.Thread(target=run_custom_job, args=(job_id, command), daemon=True)
    thread.start()
    return job


def write_json(handler: BaseHTTPRequestHandler, status: int, data: dict) -> None:
    encoded = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
    handler.send_response(status)
    handler.send_header("Content-Type", "application/json; charset=utf-8")
    handler.send_header("Content-Length", str(len(encoded)))
    handler.end_headers()
    handler.wfile.write(encoded)


def read_body(handler: BaseHTTPRequestHandler) -> dict:
    length = int(handler.headers.get("Content-Length") or 0)
    if not length:
        return {}
    raw = handler.rfile.read(length).decode("utf-8")
    return json.loads(raw) if raw.strip() else {}


def static_path(raw_path: str) -> Path | None:
    path = unquote(urlparse(raw_path).path)
    if path == "/":
        path = "/index.html"
    candidate = (STATIC_DIR / path.lstrip("/")).resolve()
    static_root = STATIC_DIR.resolve()
    if not str(candidate).casefold().startswith(str(static_root).casefold()):
        return None
    if candidate.is_file():
        return candidate
    return None


def official_docs_static_path(raw_path: str) -> Path | None:
    path = unquote(urlparse(raw_path).path)
    prefix = "/official-docs-static"
    if not path.startswith(prefix):
        return None
    relative = path[len(prefix) :].lstrip("/") or "index.html"
    candidate = (OFFICIAL_STATIC_DIR / relative).resolve()
    static_root = OFFICIAL_STATIC_DIR.resolve()
    if not str(candidate).casefold().startswith(str(static_root).casefold()):
        return None
    if candidate.is_file():
        return candidate
    return None


class ConsoleHandler(BaseHTTPRequestHandler):
    def do_GET(self) -> None:
        parsed = urlparse(self.path)
        if parsed.path == "/api/overview":
            write_json(self, 200, overview())
            return
        if parsed.path == "/api/official-docs/pages":
            write_json(self, 200, official_docs_pages(parse_qs(parsed.query)))
            return
        if parsed.path == "/api/official-docs/page-text":
            write_json(self, 200, official_docs_page_text(parse_qs(parsed.query)))
            return
        if parsed.path.startswith("/api/jobs/"):
            job_id = parsed.path.rsplit("/", 1)[-1]
            with JOB_LOCK:
                job = JOBS.get(job_id)
            write_json(self, 200 if job else 404, {"job": job})
            return
        official_path = official_docs_static_path(self.path)
        if official_path:
            data = official_path.read_bytes()
            content_type = mimetypes.guess_type(str(official_path))[0] or "application/octet-stream"
            self.send_response(200)
            self.send_header("Content-Type", content_type)
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)
            return
        path = static_path(self.path)
        if path:
            data = path.read_bytes()
            content_type = mimetypes.guess_type(str(path))[0] or "application/octet-stream"
            self.send_response(200)
            self.send_header("Content-Type", content_type)
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)
            return
        write_json(self, 404, {"error": "not found"})

    def do_POST(self) -> None:
        try:
            if urlparse(self.path).path == "/api/rag/query":
                write_json(self, 200, query_rag(read_body(self)))
                return
            if urlparse(self.path).path == "/api/1c/fact":
                write_json(self, 200, query_1c_fact(read_body(self)))
                return
            if urlparse(self.path).path == "/api/1c/route":
                write_json(self, 200, query_1c_route(read_body(self)))
                return
            if urlparse(self.path).path == "/api/1c/intake":
                write_json(self, 200, query_1c_intake(read_body(self)))
                return
            if urlparse(self.path).path == "/api/run":
                payload = read_body(self)
                job = start_job(str(payload.get("command_id") or ""))
                write_json(self, 202, {"job": job})
                return
            if urlparse(self.path).path == "/api/official-docs/fetch-target":
                payload = read_body(self)
                job = start_target_its_job(
                    str(payload.get("url") or ""),
                    max_pages=int(payload.get("max_pages") or 12),
                    max_depth=int(payload.get("max_depth") or 2),
                )
                write_json(self, 202, {"job": job})
                return
            write_json(self, 404, {"error": "not found"})
        except Exception as exc:  # noqa: BLE001
            write_json(self, 400, {"error": str(exc)})

    def log_message(self, format: str, *args) -> None:  # noqa: A003
        print(f"[management-console] {self.address_string()} - {format % args}")


def main() -> int:
    parser = argparse.ArgumentParser(description="Run local LLM/1C management console.")
    parser.add_argument("--host", default=DEFAULT_HOST)
    parser.add_argument("--port", type=int, default=DEFAULT_PORT)
    args = parser.parse_args()

    server = ThreadingHTTPServer((args.host, args.port), ConsoleHandler)
    print(f"Management console: http://{args.host}:{args.port}/")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        return 130
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
