#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
将 jsons/zxxk_material_time_categories.json 导入 MySQL xkw_categories 表。
"""

import argparse
import hashlib
import json
import os
import re
from pathlib import Path
from typing import Dict, Iterable, List, Optional

import mysql.connector

DB_CONFIG = {
    "host": os.environ.get("XKW_DB_HOST", "192.168.0.164"),
    "port": int(os.environ.get("XKW_DB_PORT", "3307")),
    "user": os.environ.get("XKW_DB_USER", "root"),
    "password": os.environ.get("XKW_DB_PASSWORD", "myP#ssw0rd"),
    "database": os.environ.get("XKW_DB_NAME", "xkw"),
    "charset": "utf8mb4",
}

BASE_DIR = Path(__file__).resolve().parent
DEFAULT_JSON_FILE_PATH = BASE_DIR / "jsons" / "zxxk_material_time_categories.json"
DEFAULT_TABLE_NAME = "xkw_categories"
DEFAULT_BATCH_SIZE = 500
SELECTED_CATEGORIES = (
    "single_file",
    "archive_within_5_minutes",
    "archive_child_later",
    "archive_child_earlier",
)

INSERT_COLUMNS = (
    "relative_path_hash",
    "file_name",
    "mtime",
    "datetime",
    "relative_path",
    "category",
    "file_kind",
    "archive_mtime",
    "archive_datetime",
    "latest_child_mtime",
    "latest_child_datetime",
    "child_archive_time_diff_seconds",
    "child_source_time_diff_seconds",
    "classification_time_basis",
    "child_count",
    "metadata_found",
    "title",
    "url",
    "source_time",
    "word_source_time",
    "zxxk_id",
    "word_doc",
    "word_row_index",
    "source_time_raw",
    "source_time_mtime",
    "raw_json",
)

CREATE_TABLE_TEMPLATE = """
CREATE TABLE IF NOT EXISTS `{table_name}` (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
    relative_path_hash CHAR(64) NOT NULL COMMENT 'relative_path/file_name 哈希，用于幂等导入',

    file_name VARCHAR(1024) COMMENT '文件名',
    mtime DOUBLE COMMENT '文件 mtime',
    `datetime` DATETIME COMMENT '文件时间',
    relative_path TEXT COMMENT '相对路径',
    category VARCHAR(64) NOT NULL COMMENT '分类',
    file_kind VARCHAR(32) COMMENT '文件类型: single_file/archive',

    archive_mtime DOUBLE COMMENT '压缩包 mtime',
    archive_datetime DATETIME COMMENT '压缩包时间',
    latest_child_mtime DOUBLE COMMENT '压缩包内最新子文件 mtime',
    latest_child_datetime DATETIME COMMENT '压缩包内最新子文件时间',
    child_archive_time_diff_seconds DOUBLE COMMENT '子文件时间 - 压缩包时间',
    child_source_time_diff_seconds DOUBLE COMMENT '子文件时间 - 数据库发布时间',
    classification_time_basis VARCHAR(64) COMMENT '分类时间依据',
    child_count INT COMMENT '压缩包内子文件数量',

    metadata_found TINYINT(1) COMMENT '是否匹配到 Word/资料元数据',
    title TEXT COMMENT '学科网资料标题',
    url TEXT COMMENT '学科网资料链接',
    source_time DATETIME COMMENT '数据库中的学科网上传日期，已规范化',
    word_source_time VARCHAR(100) COMMENT 'Word 中解析到的原始时间',
    zxxk_id VARCHAR(100) COMMENT '学科网资料 ID',
    word_doc VARCHAR(1024) COMMENT '来源 Word 文件',
    word_row_index INT COMMENT 'Word 解析行号',
    source_time_raw VARCHAR(100) COMMENT '数据库原始上传日期字符串',
    source_time_mtime DOUBLE COMMENT '数据库发布时间 timestamp',
    raw_json JSON NOT NULL COMMENT '原始分类 JSON 记录',

    imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '导入时间',
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

    UNIQUE KEY uniq_relative_path_hash (relative_path_hash),
    KEY idx_category (category),
    KEY idx_file_kind (file_kind),
    KEY idx_zxxk_id (zxxk_id),
    KEY idx_source_time (source_time),
    KEY idx_classification_time_basis (classification_time_basis)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学科网资料分类结果';
"""


def mysql_identifier(name: str) -> str:
    if not re.match(r"^[A-Za-z0-9_]+$", name):
        raise ValueError(f"非法表名：{name}")
    return name


def normalize_datetime(value) -> Optional[str]:
    if not value:
        return None
    if isinstance(value, str) and re.match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$", value):
        return value
    return None


def optional_float(value) -> Optional[float]:
    if value in (None, ""):
        return None
    return float(value)


def optional_int(value) -> Optional[int]:
    if value in (None, ""):
        return None
    return int(value)


def bool_to_int(value) -> Optional[int]:
    if value is None:
        return None
    return 1 if bool(value) else 0


def json_dumps(value) -> str:
    return json.dumps(value or {}, ensure_ascii=False)


def build_relative_path_hash(record: Dict) -> str:
    key = record.get("relative_path") or record.get("path") or record.get("file_name") or ""
    return hashlib.sha256(str(key).encode("utf-8")).hexdigest()


def normalize_record(category: str, source_record: Dict) -> Dict:
    return {
        "relative_path_hash": build_relative_path_hash(source_record),
        "file_name": source_record.get("file_name"),
        "mtime": optional_float(source_record.get("mtime")),
        "datetime": normalize_datetime(source_record.get("datetime")),
        "relative_path": source_record.get("relative_path"),
        "category": category,
        "file_kind": source_record.get("file_kind"),
        "archive_mtime": optional_float(source_record.get("archive_mtime")),
        "archive_datetime": normalize_datetime(source_record.get("archive_datetime")),
        "latest_child_mtime": optional_float(source_record.get("latest_child_mtime")),
        "latest_child_datetime": normalize_datetime(source_record.get("latest_child_datetime")),
        "child_archive_time_diff_seconds": optional_float(source_record.get("child_archive_time_diff_seconds")),
        "child_source_time_diff_seconds": optional_float(source_record.get("child_source_time_diff_seconds")),
        "classification_time_basis": source_record.get("classification_time_basis"),
        "child_count": optional_int(source_record.get("child_count")),
        "metadata_found": bool_to_int(source_record.get("metadata_found")),
        "title": source_record.get("title"),
        "url": source_record.get("url"),
        "source_time": normalize_datetime(source_record.get("source_time")),
        "word_source_time": source_record.get("word_source_time"),
        "zxxk_id": source_record.get("zxxk_id"),
        "word_doc": source_record.get("word_doc"),
        "word_row_index": optional_int(source_record.get("word_row_index")),
        "source_time_raw": source_record.get("source_time_raw"),
        "source_time_mtime": optional_float(source_record.get("source_time_mtime")),
        "raw_json": json_dumps(source_record),
    }


def normalize_records_for_import(data: Dict) -> List[Dict]:
    categories = data.get("categories") or {}
    records: List[Dict] = []
    for category in SELECTED_CATEGORIES:
        for source_record in categories.get(category, []):
            records.append(normalize_record(category, source_record))
    return records


def load_json_data(file_path: Path) -> Dict:
    with file_path.open("r", encoding="utf-8") as f:
        data = json.load(f)
    if not isinstance(data, dict):
        raise ValueError("分类 JSON 顶层必须是对象")
    return data


def ensure_table(conn, table_name: str) -> None:
    safe_table = mysql_identifier(table_name)
    cursor = conn.cursor()
    try:
        cursor.execute(CREATE_TABLE_TEMPLATE.format(table_name=safe_table))
    finally:
        cursor.close()


def build_insert_sql(table_name: str) -> str:
    safe_table = mysql_identifier(table_name)
    columns = ", ".join(f"`{column}`" for column in INSERT_COLUMNS)
    placeholders = ", ".join(["%s"] * len(INSERT_COLUMNS))
    updates = ", ".join(
        f"`{column}` = VALUES(`{column}`)"
        for column in INSERT_COLUMNS
        if column != "relative_path_hash"
    )
    return f"INSERT INTO `{safe_table}` ({columns}) VALUES ({placeholders}) ON DUPLICATE KEY UPDATE {updates}"


def iter_batches(items: List, batch_size: int):
    if batch_size <= 0:
        raise ValueError("batch_size 必须大于 0")
    for start in range(0, len(items), batch_size):
        yield items[start : start + batch_size]


def import_records(
    records: Iterable[Dict],
    table_name: str = DEFAULT_TABLE_NAME,
    db_config: Optional[Dict] = None,
    batch_size: int = DEFAULT_BATCH_SIZE,
) -> int:
    rows = list(records)
    conn = mysql.connector.connect(**(db_config or DB_CONFIG))
    try:
        ensure_table(conn, table_name)
        if not rows:
            conn.commit()
            return 0

        insert_sql = build_insert_sql(table_name)
        values = [tuple(record.get(column) for column in INSERT_COLUMNS) for record in rows]
        cursor = conn.cursor()
        try:
            for batch in iter_batches(values, batch_size):
                cursor.executemany(insert_sql, batch)
        finally:
            cursor.close()
        conn.commit()
        return len(rows)
    finally:
        conn.close()


def main() -> None:
    parser = argparse.ArgumentParser(description="导入学科网资料分类 JSON 到 MySQL")
    parser.add_argument("--json-file", type=str, default=str(DEFAULT_JSON_FILE_PATH), help="分类 JSON 文件路径")
    parser.add_argument("--table", type=str, default=DEFAULT_TABLE_NAME, help="目标表名，默认 xkw_categories")
    parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE, help=f"每批写入条数，默认 {DEFAULT_BATCH_SIZE}")
    parser.add_argument("--dry-run", action="store_true", help="只解析并打印记录数，不写入数据库")
    args = parser.parse_args()

    json_file = Path(args.json_file).expanduser().resolve()
    data = load_json_data(json_file)
    records = normalize_records_for_import(data)

    if args.dry_run:
        print(f"解析记录数：{len(records)}")
        return

    imported_count = import_records(records, table_name=args.table, batch_size=args.batch_size)
    print(f"已导入/更新 {imported_count} 条记录到 {args.table}")


if __name__ == "__main__":
    main()
