#!/usr/bin/env python3
"""
Semantic Blast Radius & Documentation Invariants Analyzer for FreeExile.
Calculates cascading downstream impact across:
1. Documentation Index & Bidirectional Cross-References (related_docs, related_code)
2. Multilingual Wiki Mirroring (wiki/vi <-> wiki/en <-> wiki/zh)
3. Domain Entity & Mechanism Extraction (Quests, Zones, Acts, Currencies, Combat formulas)
4. Game Design Matrix Database (SQLite entities and cross_relationships triples)
5. Dual-Stack Catalogs & Server/Client Code Synchronizations
"""
from __future__ import annotations

import argparse
import json
import os
import re
import sqlite3
import sys
import time
from typing import Any, Dict, List, Optional, Set, Tuple

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")
if hasattr(sys.stderr, "reconfigure"):
    sys.stderr.reconfigure(encoding="utf-8")

PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
DOCS_ROOT = os.path.join(PROJECT_ROOT, "docs")
WIKI_ROOT = os.path.join(PROJECT_ROOT, "wiki")
INDEX_JSON_PATH = os.path.join(DOCS_ROOT, "documentation_index.json")
DB_PATH = os.path.join(PROJECT_ROOT, "data", "game_design_matrix.db")
ACK_STORE_PATH = os.path.join(PROJECT_ROOT, "data", ".doc_blast_radius_ack.json")
ACK_TTL_SECONDS = 1800  # 30 minutes

CORE_DOCS = {
    "docs/architecture/GDD_BIBLE.md",
    "docs/standards/ENGINEERING_STANDARDS_2026.md",
    "docs/standards/GLOBAL_LOCALIZATION_DICTIONARY.md",
    "docs/standards/DESIGN_PIPELINE_AND_CROSS_DEPT_WORKFLOW.md",
    "docs/security/INDEPENDENT_SECURITY_AND_ANTICHEAT_CHARTER.md",
    "wiki/vi/QUESTS_AND_MILESTONES_SPECS.md",
    "wiki/vi/POE2_GAMEPLAY_AND_ZONE_ARCHITECTURE.md",
}


def _normalize_path(target: str) -> str:
    norm = target.replace("\\", "/").strip()
    if os.path.isabs(norm):
        norm = os.path.relpath(norm, PROJECT_ROOT).replace("\\", "/")
    return norm


def _get_ack_store() -> Dict[str, float]:
    if not os.path.exists(ACK_STORE_PATH):
        return {}
    try:
        with open(ACK_STORE_PATH, "r", encoding="utf-8") as f:
            data = json.load(f)
            now = time.time()
            return {k: v for k, v in data.items() if now - v < ACK_TTL_SECONDS}
    except Exception:
        return {}


def _save_ack(target: str) -> None:
    store = _get_ack_store()
    store[target] = time.time()
    os.makedirs(os.path.dirname(ACK_STORE_PATH), exist_ok=True)
    try:
        with open(ACK_STORE_PATH, "w", encoding="utf-8") as f:
            json.dump(store, f, indent=2)
    except Exception:
        pass


def _load_doc_index() -> Dict[str, Any]:
    if not os.path.exists(INDEX_JSON_PATH):
        return {}
    try:
        with open(INDEX_JSON_PATH, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return {}


def _find_frontmatter_relations(target_norm: str, doc_index: Dict[str, Any]) -> Tuple[List[str], List[str], List[str]]:
    """Returns (related_docs, reverse_related_docs, related_code)."""
    related_docs: List[str] = []
    reverse_docs: List[str] = []
    related_code: List[str] = []

    documents = doc_index.get("documents", [])
    target_entry: Optional[Dict[str, Any]] = None

    for doc in documents:
        r_path = doc.get("rel_path", "").replace("\\", "/")
        if r_path == target_norm or os.path.basename(r_path) == os.path.basename(target_norm):
            target_entry = doc
            break

    if target_entry:
        for rd in target_entry.get("related_docs", []):
            clean_rd = rd.replace("\\", "/")
            if clean_rd != target_norm:
                related_docs.append(clean_rd)
        for rc in target_entry.get("related_code", []):
            related_code.append(rc.replace("\\", "/"))

    # Find reverse dependencies (who points to target?)
    for doc in documents:
        r_path = doc.get("rel_path", "").replace("\\", "/")
        if r_path == target_norm:
            continue
        rd_list = [x.replace("\\", "/") for x in doc.get("related_docs", [])]
        if target_norm in rd_list or (target_entry and target_entry.get("doc_id") in rd_list):
            reverse_docs.append(r_path)

    return sorted(list(set(related_docs))), sorted(list(set(reverse_docs))), sorted(list(set(related_code)))


def _find_multilingual_counterparts(target_norm: str) -> List[str]:
    """Find corresponding files across wiki/vi, wiki/en, wiki/zh."""
    counterparts: List[str] = []
    parts = target_norm.split("/")
    
    if len(parts) >= 3 and parts[0] == "wiki" and parts[1] in ("vi", "en", "zh"):
        lang = parts[1]
        sub_path = "/".join(parts[2:])
        other_langs = [l for l in ("vi", "en", "zh") if l != lang]
        for ol in other_langs:
            cand = f"wiki/{ol}/{sub_path}"
            if os.path.exists(os.path.join(PROJECT_ROOT, cand)):
                counterparts.append(cand)
    elif target_norm.startswith("docs/"):
        # Check if wiki has specs with same base filename
        base_name = os.path.basename(target_norm)
        for lang in ("vi", "en", "zh"):
            cand = f"wiki/{lang}/{base_name}"
            if os.path.exists(os.path.join(PROJECT_ROOT, cand)):
                counterparts.append(cand)

    return sorted(list(set(counterparts)))


def _extract_semantic_entities(content: str) -> Set[str]:
    """Extract IDs and core domain keywords from markdown text."""
    entities: Set[str] = set()
    
    # 1. Regex ID patterns
    patterns = [
        r"\b(quest_[a-z0-9_]+)\b",
        r"\b(zone_[a-z0-9_]+)\b",
        r"\b(npc_[a-z0-9_]+)\b",
        r"\b(ACT_[I|V|X]+[A-Z0-9_]*)\b",
    ]
    for p in patterns:
        for match in re.findall(p, content, re.IGNORECASE):
            entities.add(match)

    # 2. Key gameplay mechanics terms
    keywords = [
        "huyễn ảnh bộ",
        "i-frame",
        "tax_rate",
        "thuế sàn",
        "huyết hồn thạch",
        "cổ cốt",
        "u minh cốt đinh",
        "2pc",
        "two-phase commit",
        "remove-only",
        "starter-finisher",
        "affix_tier",
    ]
    content_lower = content.lower()
    for kw in keywords:
        if kw in content_lower:
            entities.add(kw)

    return entities


def _scan_semantic_affinities(target_norm: str, entities: Set[str]) -> List[str]:
    """Scans all markdown files in docs/ and wiki/ for shared critical entities."""
    if not entities:
        return []

    affinities: List[str] = []
    scan_dirs = [DOCS_ROOT, WIKI_ROOT]

    for s_dir in scan_dirs:
        if not os.path.exists(s_dir):
            continue
        for root, dirs, files in os.walk(s_dir):
            if any(ig in root for ig in [".git", "node_modules", "__pycache__", "design_requests"]):
                continue
            for f in files:
                if not f.endswith(".md"):
                    continue
                file_rel = os.path.relpath(os.path.join(root, f), PROJECT_ROOT).replace("\\", "/")
                if file_rel == target_norm:
                    continue
                try:
                    with open(os.path.join(root, f), "r", encoding="utf-8", errors="ignore") as src:
                        text = src.read().lower()
                    # If file shares at least 2 distinct entities or a primary entity ID
                    shared_count = 0
                    for ent in entities:
                        if ent.lower() in text:
                            shared_count += 1
                            if ent.startswith("quest_") or ent.startswith("zone_") or ent.startswith("act_"):
                                affinities.append(file_rel)
                                break
                    if shared_count >= 2:
                        affinities.append(file_rel)
                except Exception:
                    continue

    return sorted(list(set(affinities)))


def _check_db_and_catalogs(entities: Set[str]) -> Tuple[List[str], List[str]]:
    """Checks database impact and catalog counterparts."""
    db_impacts: List[str] = []
    affected_catalogs: List[str] = []

    if os.path.exists(DB_PATH):
        try:
            conn = sqlite3.connect(DB_PATH)
            cur = conn.cursor()
            for ent in entities:
                if ent.startswith("quest_"):
                    cur.execute("SELECT title, act_id, zone_id FROM quests WHERE quest_id = ?", (ent,))
                    row = cur.fetchone()
                    if row:
                        db_impacts.append(f"Quest '{ent}' ({row[0]}) tại Act {row[1]}, Zone {row[2]}")
                elif ent.startswith("zone_"):
                    cur.execute("SELECT name, act_id FROM zones WHERE zone_id = ?", (ent,))
                    row = cur.fetchone()
                    if row:
                        db_impacts.append(f"Zone '{ent}' ({row[0]}) tại Act {row[1]}")
                elif ent.startswith("ACT_"):
                    cur.execute("SELECT relation_type, target_id FROM cross_relationships WHERE source_id = ?", (ent,))
                    rels = cur.fetchall()
                    if rels:
                        db_impacts.append(f"Act '{ent}' có {len(rels)} liên kết triples trong cross_relationships.")
            conn.close()
        except Exception:
            pass

    # Catalog matches
    cat_keywords = {
        "quest": ("server/world/quest_engine.py", "server/world/quest_catalog.py"),
        "zone": ("server/world/zone_engine.py", "server/world/encounter_zones_catalog.py"),
        "cổ cốt": ("server/world/economy_catalog.py", "client/webapp/js/data/economy_catalog.js"),
        "tax_rate": ("server/world/economy_catalog.py", "server/trade/consignment_vault.py"),
        "huyết hồn thạch": ("server/world/economy_catalog.py", "client/webapp/js/data/economy_catalog.js"),
        "i-frame": ("server/world/combat_engine.py", "client/webapp/js/core/isometric_renderer.js"),
    }
    for ent in entities:
        ent_lower = ent.lower()
        for kw, paths in cat_keywords.items():
            if kw in ent_lower:
                for p in paths:
                    if os.path.exists(os.path.join(PROJECT_ROOT, p)):
                        affected_catalogs.append(p)

    return sorted(list(set(db_impacts))), sorted(list(set(affected_catalogs)))


def analyze_doc_blast_radius(target: str, ack: bool = False) -> Dict[str, Any]:
    norm_target = _normalize_path(target)
    target_abs = os.path.join(PROJECT_ROOT, norm_target.replace("/", os.sep))

    doc_index = _load_doc_index()
    related_docs, reverse_docs, related_code = _find_frontmatter_relations(norm_target, doc_index)
    multilingual_counterparts = _find_multilingual_counterparts(norm_target)

    # Read content to extract entities if file exists
    entities: Set[str] = set()
    if os.path.exists(target_abs):
        try:
            with open(target_abs, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
            entities = _extract_semantic_entities(content)
        except Exception:
            pass

    semantic_affinities = _scan_semantic_affinities(norm_target, entities)
    db_impacts, affected_catalogs = _check_db_and_catalogs(entities)

    all_affected_docs = sorted(list(set(
        related_docs + reverse_docs + multilingual_counterparts + semantic_affinities
    )))
    all_affected_code = sorted(list(set(related_code + affected_catalogs)))

    total_dependents = len(all_affected_docs) + len(all_affected_code)
    is_core = norm_target in CORE_DOCS or any(cd in norm_target for cd in ["GDD_BIBLE", "STANDARDS", "CHARTER"])

    # Determine risk level
    if norm_target.startswith("tests/") or "scratch" in norm_target or "design_requests" in norm_target:
        risk_level = "LOW"
    elif is_core or total_dependents >= 5:
        risk_level = "CRITICAL"
    elif total_dependents >= 2 or len(db_impacts) > 0:
        risk_level = "HIGH"
    elif total_dependents == 1:
        risk_level = "MEDIUM"
    else:
        risk_level = "LOW"

    if ack:
        _save_ack(norm_target)
        acknowledged = True
    else:
        store = _get_ack_store()
        acknowledged = norm_target in store

    blocked = False
    if risk_level in ("HIGH", "CRITICAL") and not acknowledged:
        blocked = True
        reason = (
            f"[DOC BLAST RADIUS BLOCKED] Tài liệu '{norm_target}' có mức độ rủi ro {risk_level} "
            f"kéo theo {len(all_affected_docs)} tài liệu, {len(all_affected_code)} file code, "
            f"và {len(db_impacts)} ràng buộc SQLite liên đới. "
            f"Bắt buộc Agent chạy 'python tools/analysis/doc_blast_radius.py --target {norm_target} --ack' "
            f"để đối soát danh sách và mở khóa chỉnh sửa!"
        )
    else:
        reason = f"Đã thông qua kiểm tra Semantic Doc Blast Radius (Rủi ro: {risk_level}, Acknowledged: {acknowledged})."

    return {
        "target": norm_target,
        "risk_level": risk_level,
        "blocked": blocked,
        "acknowledged": acknowledged,
        "upstream_and_related_docs": related_docs,
        "reverse_dependent_docs": reverse_docs,
        "multilingual_counterparts": multilingual_counterparts,
        "semantic_affinities": semantic_affinities,
        "all_affected_docs": all_affected_docs,
        "affected_code_catalogs": all_affected_code,
        "database_impacts": db_impacts,
        "extracted_entities": sorted(list(entities)),
        "reason": reason,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Semantic Blast Radius & Documentation Invariants Analyzer")
    parser.add_argument("--target", required=True, help="Target markdown file path or concept ID to analyze")
    parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
    parser.add_argument("--ack", action="store_true", help="Acknowledge understanding and unlock target for 30 minutes")
    args = parser.parse_args()

    result = analyze_doc_blast_radius(args.target, ack=args.ack)

    if args.format == "json":
        print(json.dumps(result, indent=2, ensure_ascii=True))
    else:
        print(f"=== BÁO CÁO PHÂN TÍCH VÙNG ẢNH HƯỞNG TÀI LIỆU & NGỮ NGHĨA (DOC BLAST RADIUS) ===")
        print(f"Mục tiêu        : {result['target']}")
        print(f"Mức độ rủi ro   : {result['risk_level']}")
        print(f"Trạng thái khóa : {'[BLOCKED] ĐANG BỊ KHÓA' if result['blocked'] else '[ALLOWED] ĐÃ MỞ KHÓA'}")
        print(f"Đã duyệt (Ack)  : {'Có' if result['acknowledged'] else 'Chưa'}")
        
        if result["all_affected_docs"]:
            print(f"\nTài liệu kéo theo cần đối soát ({len(result['all_affected_docs'])}):")
            for d in result["all_affected_docs"]:
                tag = ""
                if d in result["multilingual_counterparts"]:
                    tag = " [Wiki Mirror]"
                elif d in result["reverse_dependent_docs"]:
                    tag = " [Reverse Ref]"
                elif d in result["upstream_and_related_docs"]:
                    tag = " [Related Doc]"
                elif d in result["semantic_affinities"]:
                    tag = " [Shared Entity/Concept]"
                print(f"  - {d}{tag}")

        if result["affected_code_catalogs"]:
            print(f"\nCode Catalogs & Services liên đới ({len(result['affected_code_catalogs'])}):")
            for c in result["affected_code_catalogs"]:
                print(f"  * {c}")

        if result["database_impacts"]:
            print(f"\nRàng buộc SQLite Game Design Matrix ({len(result['database_impacts'])}):")
            for imp in result["database_impacts"]:
                print(f"  # {imp}")

        if result["extracted_entities"]:
            print(f"\nThực thể & Cơ chế nhận diện được ({len(result['extracted_entities'])}):")
            print(f"  {', '.join(result['extracted_entities'][:12])}")

        print(f"\nChi tiết: {result['reason']}")

    return 0


if __name__ == "__main__":
    sys.exit(main())
