# -*- coding: utf-8 -*-
"""
文档对齐分析器 — 检查文档与代码之间的映射关系、过期点、冲突点和缺失点。

文档来源：
- docs/ 目录（.md, .txt, .csv, .json）
- 根目录 README.md
- 各模块内的 README.md
- .kiro/steering/ 引导文件
- docs/test-json-doc/ API 响应样本
"""

from __future__ import annotations

import json
import re
from datetime import datetime, timezone
from pathlib import Path

from scripts.audit import AlignmentIssue, DocMapping

# ---------------------------------------------------------------------------
# 常量
# ---------------------------------------------------------------------------

# 文档文件扩展名
_DOC_EXTENSIONS = {".md", ".txt", ".csv"}

# 核心代码目录——缺少文档时应报告
_CORE_CODE_DIRS = {
    "tasks",
    "loaders",
    "orchestration",
    "quality",
    "models",
    "utils",
    "api",
    "scd",
    "config",
    "database",
}

# ODS 表中的通用元数据列，比对时忽略
_ODS_META_COLUMNS = {"content_hash", "payload", "created_at", "updated_at", "id"}

# SQL 关键字，解析 DDL 列名时排除
_SQL_KEYWORDS = {
    "primary", "key", "not", "null", "default", "unique", "check",
    "references", "foreign", "constraint", "index", "create", "table",
    "if", "exists", "serial", "bigserial", "true", "false",
}


# ---------------------------------------------------------------------------
# 安全读取文件（编码回退）
# ---------------------------------------------------------------------------

def _safe_read(path: Path) -> str:
    """尝试以 utf-8 → gbk → latin-1 回退读取文件内容。"""
    for enc in ("utf-8", "gbk", "latin-1"):
        try:
            return path.read_text(encoding=enc)
        except (UnicodeDecodeError, UnicodeError):
            continue
    return ""


# ---------------------------------------------------------------------------
# scan_docs — 扫描所有文档来源
# ---------------------------------------------------------------------------

def scan_docs(repo_root: Path) -> list[str]:
    """扫描所有文档文件路径，返回相对路径列表（已排序）。

    文档来源：
    1. docs/ 目录下的 .md, .txt, .csv, .json 文件
    2. 根目录 README.md
    3. 各模块内的 README.md（如 gui/README.md）
    4. .kiro/steering/ 引导文件
    """
    results: list[str] = []

    def _rel(p: Path) -> str:
        """返回归一化的正斜杠相对路径。"""
        return str(p.relative_to(repo_root)).replace("\\", "/")

    # 1. docs/ 目录（递归，含 test-json-doc 下的 .json）
    docs_dir = repo_root / "docs"
    if docs_dir.is_dir():
        for p in docs_dir.rglob("*"):
            if p.is_file():
                ext = p.suffix.lower()
                if ext in _DOC_EXTENSIONS or ext == ".json":
                    results.append(_rel(p))

    # 2. 根目录 README.md
    root_readme = repo_root / "README.md"
    if root_readme.is_file():
        results.append("README.md")

    # 3. 各模块内的 README.md
    for child in sorted(repo_root.iterdir()):
        if child.is_dir() and child.name not in ("docs", ".kiro"):
            readme = child / "README.md"
            if readme.is_file():
                results.append(_rel(readme))

    # 4. .kiro/steering/
    steering_dir = repo_root / ".kiro" / "steering"
    if steering_dir.is_dir():
        for p in sorted(steering_dir.iterdir()):
            if p.is_file():
                results.append(_rel(p))

    return sorted(set(results))


# ---------------------------------------------------------------------------
# extract_code_references — 从文档提取代码引用
# ---------------------------------------------------------------------------

def extract_code_references(doc_path: Path) -> list[str]:
    """从文档中提取代码引用（反引号内的文件路径、类名、函数名等）。

    规则：
    - 提取反引号内的内容
    - 跳过单字符引用
    - 跳过纯数字/版本号
    - 反斜杠归一化为正斜杠
    - 去重
    """
    if not doc_path.is_file():
        return []

    text = _safe_read(doc_path)
    if not text:
        return []

    # 提取反引号内容
    backtick_refs = re.findall(r"`([^`]+)`", text)

    seen: set[str] = set()
    results: list[str] = []

    for raw in backtick_refs:
        ref = raw.strip()
        # 归一化反斜杠
        ref = ref.replace("\\", "/")
        # 跳过单字符
        if len(ref) <= 1:
            continue
        # 跳过纯数字和版本号
        if re.fullmatch(r"[\d.]+", ref):
            continue
        # 去重
        if ref in seen:
            continue
        seen.add(ref)
        results.append(ref)

    return results


# ---------------------------------------------------------------------------
# check_reference_validity — 检查引用有效性
# ---------------------------------------------------------------------------

def check_reference_validity(ref: str, repo_root: Path) -> bool:
    """检查文档中的代码引用是否仍然有效。

    检查策略：
    1. 直接作为文件/目录路径检查
    2. 去掉 FQ-ETL/ 前缀后检查（兼容旧文档引用）
    3. 将点号路径转为文件路径检查（如 config.settings → config/settings.py）
    """
    # 1. 直接路径
    if (repo_root / ref).exists():
        return True

    # 2. 去掉旧包名前缀（兼容历史文档）
    for prefix in ("FQ-ETL/", "etl_billiards/"):
        if ref.startswith(prefix):
            stripped = ref[len(prefix):]
            if (repo_root / stripped).exists():
                return True

    # 3. 点号模块路径 → 文件路径
    if "." in ref and "/" not in ref:
        as_path = ref.replace(".", "/") + ".py"
        if (repo_root / as_path).exists():
            return True
        # 也可能是目录（包）
        as_dir = ref.replace(".", "/")
        if (repo_root / as_dir).is_dir():
            return True

    return False


# ---------------------------------------------------------------------------
# find_undocumented_modules — 找出缺少文档的核心代码模块
# ---------------------------------------------------------------------------

def find_undocumented_modules(
    repo_root: Path,
    documented: set[str],
) -> list[str]:
    """找出缺少文档的核心代码模块。

    只检查 _CORE_CODE_DIRS 中的 .py 文件（排除 __init__.py）。
    返回已排序的相对路径列表。
    """
    undocumented: list[str] = []

    for core_dir in sorted(_CORE_CODE_DIRS):
        dir_path = repo_root / core_dir
        if not dir_path.is_dir():
            continue
        for py_file in dir_path.rglob("*.py"):
            if py_file.name == "__init__.py":
                continue
            rel = str(py_file.relative_to(repo_root))
            # 归一化路径分隔符
            rel = rel.replace("\\", "/")
            if rel not in documented:
                undocumented.append(rel)

    return sorted(undocumented)


# ---------------------------------------------------------------------------
# DDL / 数据字典解析辅助函数
# ---------------------------------------------------------------------------

def _parse_ddl_tables(sql: str) -> dict[str, set[str]]:
    """从 DDL SQL 中提取表名和列名。

    返回 {表名: {列名集合}} 字典。
    支持带 schema 前缀的表名（如 dwd.dim_member → dim_member）。
    """
    tables: dict[str, set[str]] = {}

    # 匹配 CREATE TABLE [IF NOT EXISTS] [schema.]table_name (
    create_re = re.compile(
        r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"
        r"(?:\w+\.)?(\w+)\s*\(",
        re.IGNORECASE,
    )

    for match in create_re.finditer(sql):
        table_name = match.group(1)
        # 找到对应的括号内容
        start = match.end()
        depth = 1
        pos = start
        while pos < len(sql) and depth > 0:
            if sql[pos] == "(":
                depth += 1
            elif sql[pos] == ")":
                depth -= 1
            pos += 1
        body = sql[start:pos - 1]

        columns: set[str] = set()
        # 逐行提取列名——取每行第一个标识符
        for line in body.split("\n"):
            line = line.strip().rstrip(",")
            if not line:
                continue
            # 提取第一个单词
            col_match = re.match(r"(\w+)", line)
            if col_match:
                col_name = col_match.group(1).lower()
                # 排除 SQL 关键字
                if col_name not in _SQL_KEYWORDS:
                    columns.add(col_name)

        tables[table_name] = columns

    return tables


def _parse_dictionary_tables(md: str) -> dict[str, set[str]]:
    """从数据字典 Markdown 中提取表名和字段名。

    约定：
    - 表名出现在 ## 标题中（可能带反引号）
    - 字段名出现在 Markdown 表格的第一列
    - 跳过表头行（含"字段"字样）和分隔行（含 ---）
    """
    tables: dict[str, set[str]] = {}
    current_table: str | None = None

    for line in md.split("\n"):
        # 匹配 ## 标题中的表名
        heading = re.match(r"^##\s+`?(\w+)`?", line)
        if heading:
            current_table = heading.group(1)
            tables[current_table] = set()
            continue

        if current_table is None:
            continue

        # 跳过分隔行
        if re.match(r"^\s*\|[-\s|]+\|\s*$", line):
            continue

        # 解析表格行
        row_match = re.match(r"^\s*\|\s*(\S+)", line)
        if row_match:
            field = row_match.group(1)
            # 跳过表头（含"字段"字样）
            if field in ("字段",):
                continue
            tables[current_table].add(field)

    return tables


# ---------------------------------------------------------------------------
# check_ddl_vs_dictionary — DDL 与数据字典比对
# ---------------------------------------------------------------------------

def check_ddl_vs_dictionary(repo_root: Path) -> list[AlignmentIssue]:
    """比对 DDL 文件与数据字典文档的覆盖度。

    检查：
    1. DDL 中有但字典中没有的表 → missing
    2. 同名表中 DDL 有但字典没有的列 → conflict
    """
    issues: list[AlignmentIssue] = []

    # 收集所有 DDL 表定义
    ddl_tables: dict[str, set[str]] = {}
    db_dir = repo_root / "database"
    if db_dir.is_dir():
        for sql_file in sorted(db_dir.glob("schema_*.sql")):
            content = _safe_read(sql_file)
            for tbl, cols in _parse_ddl_tables(content).items():
                if tbl in ddl_tables:
                    ddl_tables[tbl] |= cols
                else:
                    ddl_tables[tbl] = set(cols)

    # 收集所有数据字典表定义
    dict_tables: dict[str, set[str]] = {}
    docs_dir = repo_root / "docs"
    if docs_dir.is_dir():
        for dict_file in sorted(docs_dir.glob("*dictionary*.md")):
            content = _safe_read(dict_file)
            for tbl, fields in _parse_dictionary_tables(content).items():
                if tbl in dict_tables:
                    dict_tables[tbl] |= fields
                else:
                    dict_tables[tbl] = set(fields)

    # 比对
    for tbl, ddl_cols in sorted(ddl_tables.items()):
        if tbl not in dict_tables:
            issues.append(AlignmentIssue(
                doc_path="docs/*dictionary*.md",
                issue_type="missing",
                description=f"DDL 定义了表 `{tbl}`，但数据字典中未收录",
                related_code=f"database/schema_*.sql ({tbl})",
            ))
        else:
            # 检查列差异
            dict_cols = dict_tables[tbl]
            missing_cols = ddl_cols - dict_cols
            for col in sorted(missing_cols):
                issues.append(AlignmentIssue(
                    doc_path="docs/*dictionary*.md",
                    issue_type="conflict",
                    description=f"表 `{tbl}` 的列 `{col}` 在 DDL 中存在但数据字典中缺失",
                    related_code=f"database/schema_*.sql ({tbl}.{col})",
                ))

    return issues


# ---------------------------------------------------------------------------
# check_api_samples_vs_parsers — API 样本与解析器比对
# ---------------------------------------------------------------------------

def check_api_samples_vs_parsers(repo_root: Path) -> list[AlignmentIssue]:
    """比对 API 响应样本与 ODS 表结构的一致性。

    策略：
    1. 扫描 docs/test-json-doc/ 下的 .json 文件
    2. 提取 JSON 中的顶层字段名
    3. 从 ODS DDL 中查找同名表
    4. 比对字段差异（忽略 ODS 元数据列）
    """
    issues: list[AlignmentIssue] = []

    sample_dir = repo_root / "docs" / "test-json-doc"
    if not sample_dir.is_dir():
        return issues

    # 收集 ODS 表定义（保留全部列，比对时忽略元数据列）
    ods_tables: dict[str, set[str]] = {}
    db_dir = repo_root / "database"
    if db_dir.is_dir():
        for sql_file in sorted(db_dir.glob("schema_*ODS*.sql")):
            content = _safe_read(sql_file)
            for tbl, cols in _parse_ddl_tables(content).items():
                ods_tables[tbl] = cols

    # 逐个样本文件比对
    for json_file in sorted(sample_dir.glob("*.json")):
        entity_name = json_file.stem  # 文件名（不含扩展名）作为实体名

        # 解析 JSON 样本
        try:
            content = _safe_read(json_file)
            data = json.loads(content)
        except (json.JSONDecodeError, ValueError):
            continue

        # 提取顶层字段名
        sample_fields: set[str] = set()
        if isinstance(data, list) and data:
            # 数组格式——取第一个元素的键
            first = data[0]
            if isinstance(first, dict):
                sample_fields = set(first.keys())
        elif isinstance(data, dict):
            sample_fields = set(data.keys())

        if not sample_fields:
            continue

        # 查找匹配的 ODS 表
        matched_table: str | None = None
        matched_cols: set[str] = set()
        for tbl, cols in ods_tables.items():
            # 表名包含实体名（如 test_entity 匹配 ods.test_entity）
            tbl_lower = tbl.lower()
            entity_lower = entity_name.lower()
            if entity_lower in tbl_lower or tbl_lower == entity_lower:
                matched_table = tbl
                matched_cols = cols
                break

        if matched_table is None:
            continue

        # 比对：样本中有但 ODS 表中没有的字段
        extra_fields = sample_fields - matched_cols
        for field in sorted(extra_fields):
            issues.append(AlignmentIssue(
                doc_path=f"docs/test-json-doc/{json_file.name}",
                issue_type="conflict",
                description=(
                    f"API 样本字段 `{field}` 在 ODS 表 `{matched_table}` 中未定义"
                ),
                related_code=f"database/schema_*ODS*.sql ({matched_table})",
            ))

    return issues


# ---------------------------------------------------------------------------
# build_mappings — 构建文档与代码的映射关系
# ---------------------------------------------------------------------------

def build_mappings(
    doc_paths: list[str],
    repo_root: Path,
) -> list[DocMapping]:
    """为每份文档建立与代码模块的映射关系。"""
    mappings: list[DocMapping] = []

    for doc_rel in doc_paths:
        doc_path = repo_root / doc_rel
        refs = extract_code_references(doc_path)

        # 确定关联代码和状态
        valid_refs: list[str] = []
        has_stale = False
        for ref in refs:
            if check_reference_validity(ref, repo_root):
                valid_refs.append(ref)
            else:
                has_stale = True

        # 推断文档主题（取文件名或第一行标题）
        topic = _infer_topic(doc_path, doc_rel)

        if not refs:
            status = "orphan"
        elif has_stale:
            status = "stale"
        else:
            status = "aligned"

        mappings.append(DocMapping(
            doc_path=doc_rel,
            doc_topic=topic,
            related_code=valid_refs,
            status=status,
        ))

    return mappings


def _infer_topic(doc_path: Path, doc_rel: str) -> str:
    """从文档推断主题——优先取 Markdown 一级标题，否则用文件名。"""
    if doc_path.is_file() and doc_path.suffix.lower() in (".md", ".txt"):
        try:
            text = _safe_read(doc_path)
            for line in text.split("\n"):
                line = line.strip()
                if line.startswith("# "):
                    return line[2:].strip()
        except Exception:
            pass
    return doc_rel


# ---------------------------------------------------------------------------
# render_alignment_report — 生成 Markdown 格式的文档对齐报告
# ---------------------------------------------------------------------------

def render_alignment_report(
    mappings: list[DocMapping],
    issues: list[AlignmentIssue],
    repo_root: str,
) -> str:
    """生成 Markdown 格式的文档对齐报告。

    分区：映射关系表、过期点列表、冲突点列表、缺失点列表、统计摘要。
    """
    lines: list[str] = []

    # --- 头部 ---
    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    lines.append("# 文档对齐报告")
    lines.append("")
    lines.append(f"- 生成时间：{now}")
    lines.append(f"- 仓库路径：`{repo_root}`")
    lines.append("")

    # --- 映射关系 ---
    lines.append("## 映射关系")
    lines.append("")
    if mappings:
        lines.append("| 文档路径 | 主题 | 关联代码 | 状态 |")
        lines.append("|---|---|---|---|")
        for m in mappings:
            code_str = ", ".join(f"`{c}`" for c in m.related_code) if m.related_code else "—"
            lines.append(f"| `{m.doc_path}` | {m.doc_topic} | {code_str} | {m.status} |")
    else:
        lines.append("未发现文档映射关系。")
    lines.append("")

    # --- 按 issue_type 分组 ---
    stale = [i for i in issues if i.issue_type == "stale"]
    conflict = [i for i in issues if i.issue_type == "conflict"]
    missing = [i for i in issues if i.issue_type == "missing"]

    # --- 过期点 ---
    lines.append("## 过期点")
    lines.append("")
    if stale:
        lines.append("| 文档路径 | 描述 | 关联代码 |")
        lines.append("|---|---|---|")
        for i in stale:
            lines.append(f"| `{i.doc_path}` | {i.description} | `{i.related_code}` |")
    else:
        lines.append("未发现过期点。")
    lines.append("")

    # --- 冲突点 ---
    lines.append("## 冲突点")
    lines.append("")
    if conflict:
        lines.append("| 文档路径 | 描述 | 关联代码 |")
        lines.append("|---|---|---|")
        for i in conflict:
            lines.append(f"| `{i.doc_path}` | {i.description} | `{i.related_code}` |")
    else:
        lines.append("未发现冲突点。")
    lines.append("")

    # --- 缺失点 ---
    lines.append("## 缺失点")
    lines.append("")
    if missing:
        lines.append("| 文档路径 | 描述 | 关联代码 |")
        lines.append("|---|---|---|")
        for i in missing:
            lines.append(f"| `{i.doc_path}` | {i.description} | `{i.related_code}` |")
    else:
        lines.append("未发现缺失点。")
    lines.append("")

    # --- 统计摘要 ---
    lines.append("## 统计摘要")
    lines.append("")
    lines.append(f"- 文档总数：{len(mappings)}")
    lines.append(f"- 过期点数量：{len(stale)}")
    lines.append(f"- 冲突点数量：{len(conflict)}")
    lines.append(f"- 缺失点数量：{len(missing)}")
    lines.append("")

    return "\n".join(lines)
