"""
Wildcard (glob) 路径镜像同步模块。

当 local:// RAG 的路径包含通配符时，将匹配到的文件增量同步到
~/.auto-coder-rag/wildcard/<expr_md5>/data/ 下，并通过
~/.auto-coder-rag/wildcard/<expr_md5>/.meta/sync.json 记录同步状态。
"""

import glob
import hashlib
import json
import os
import shutil
import tempfile
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from filelock import FileLock
from loguru import logger

WILDCARD_CHARS = {"*", "?", "["}

MIRROR_ROOT = Path.home() / ".auto-coder-rag" / "wildcard"

META_VERSION = 1


def is_wildcard_path(path_str: str) -> bool:
    """判断路径字符串是否包含 glob 通配符。"""
    return any(ch in path_str for ch in WILDCARD_CHARS)


def _normalize_expr(raw_expr: str, base_dir: Optional[str] = None) -> str:
    """对 wildcard 表达式做 expanduser / expandvars，若为相对则拼接 base_dir。"""
    expanded = os.path.expandvars(raw_expr)
    p = Path(expanded).expanduser()
    if not p.is_absolute():
        base = (
            Path(os.path.expandvars(base_dir)).expanduser() if base_dir else Path.cwd()
        )
        p = base / p
    return str(p.absolute())


def _expr_md5(expr: str) -> str:
    return hashlib.md5(expr.encode("utf-8")).hexdigest()


def _split_root_pattern(expr: str) -> Tuple[str, str]:
    """
    从 wildcard 表达式推导 root 和 pattern。

    从左到右找到第一个包含通配符字符的 path part，
    root = 该 part 之前的所有 part 拼接，
    pattern = 剩余 part 拼接。
    """
    parts = Path(expr).parts
    split_idx = 0
    for i, part in enumerate(parts):
        if any(ch in part for ch in WILDCARD_CHARS):
            split_idx = i
            break
    root = str(Path(*parts[:split_idx])) if split_idx > 0 else str(Path(parts[0]))
    pattern = str(Path(*parts[split_idx:])) if split_idx < len(parts) else ""
    return root, pattern


def _safe_rel_path(rel: Path) -> Path:
    """
    将相对路径中以 '.' 开头的目录/文件名映射为 '_dot_' 前缀，
    避免被 RAG os.walk 跳过隐藏目录。
    """
    safe_parts = []
    for part in rel.parts:
        if part.startswith("."):
            safe_parts.append("_dot_" + part[1:])
        else:
            safe_parts.append(part)
    return Path(*safe_parts)


def _expand_wildcard(expr: str) -> List[Path]:
    """展开 glob 表达式，返回所有匹配的文件（递归展开目录）。"""
    matched = glob.glob(expr, recursive=True)
    result: List[Path] = []
    for m in matched:
        p = Path(m)
        if p.is_file():
            result.append(p)
        elif p.is_dir():
            for root_dir, _dirs, files in os.walk(p, followlinks=True):
                for f in files:
                    result.append(Path(root_dir) / f)
    return sorted(set(result))


def _load_meta(meta_path: Path) -> Dict:
    if meta_path.exists():
        try:
            with open(meta_path, "r", encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError) as e:
            logger.warning(f"无法读取 wildcard meta 文件，将重建: {e}")
    return {}


def _save_meta(meta_path: Path, meta: Dict) -> None:
    meta_path.parent.mkdir(parents=True, exist_ok=True)
    tmp_fd, tmp_path = tempfile.mkstemp(dir=str(meta_path.parent), suffix=".tmp")
    try:
        with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)
        shutil.move(tmp_path, str(meta_path))
    except Exception:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)
        raise


def sync_wildcard_to_mirror(
    raw_expr: str,
    base_dir: Optional[str] = None,
) -> Path:
    """
    将 wildcard 表达式匹配的文件增量同步到镜像目录。

    使用跨平台文件锁（filelock）保证多进程并发安全：同一 wildcard
    表达式的同步操作互斥执行，避免 meta 覆盖和文件读写竞争。

    返回镜像数据目录 (``<mirror_root>/data``)，可直接作为 RAG 的 ``doc_dir``。

    Raises:
        FileNotFoundError: root 不存在或无匹配文件。
    """
    expr = _normalize_expr(raw_expr, base_dir)
    md5 = _expr_md5(expr)
    root, pattern = _split_root_pattern(expr)

    logger.info(
        f"Wildcard sync: expr={expr}, md5={md5}, root={root}, pattern={pattern}"
    )

    if not Path(root).exists():
        raise FileNotFoundError(
            f"Wildcard 表达式的根路径不存在: root={root}, "
            f"原始表达式={raw_expr}, 展开后={expr}"
        )

    matched_files = _expand_wildcard(expr)
    if not matched_files:
        raise FileNotFoundError(
            f"Wildcard 表达式无匹配文件: {expr} " f"(root={root}, pattern={pattern})"
        )

    mirror_dir = MIRROR_ROOT / md5
    data_dir = mirror_dir / "data"
    meta_path = mirror_dir / ".meta" / "sync.json"
    lock_path = mirror_dir / ".meta" / "sync.lock"

    data_dir.mkdir(parents=True, exist_ok=True)
    lock_path.parent.mkdir(parents=True, exist_ok=True)

    lock = FileLock(str(lock_path))
    with lock:
        return _sync_locked(
            expr, raw_expr, root, pattern, matched_files, data_dir, meta_path
        )


def _sync_locked(
    expr: str,
    raw_expr: str,
    root: str,
    pattern: str,
    matched_files: List[Path],
    data_dir: Path,
    meta_path: Path,
) -> Path:
    """在持有文件锁的情况下执行实际的同步逻辑。"""
    meta = _load_meta(meta_path)
    old_files: Dict[str, Dict] = meta.get("files", {})
    new_files: Dict[str, Dict] = {}

    root_path = Path(root)
    copied_count = 0
    skipped_count = 0

    for src in matched_files:
        src_abs = str(src)
        try:
            stat = src.stat()
        except OSError as e:
            logger.warning(f"无法 stat 文件 {src_abs}: {e}")
            continue

        try:
            rel = src.relative_to(root_path)
        except ValueError:
            rel = Path(src.name)

        safe_rel = _safe_rel_path(rel)
        dst = data_dir / safe_rel

        entry = old_files.get(src_abs)
        if (
            entry
            and entry.get("mtime") == stat.st_mtime
            and entry.get("size") == stat.st_size
            and dst.exists()
        ):
            new_files[src_abs] = entry
            skipped_count += 1
            continue

        dst.parent.mkdir(parents=True, exist_ok=True)
        try:
            shutil.copy2(str(src), str(dst))
        except OSError as e:
            logger.error(f"拷贝失败 {src_abs} -> {dst}: {e}")
            continue

        new_files[src_abs] = {
            "dst_rel": str(safe_rel),
            "mtime": stat.st_mtime,
            "size": stat.st_size,
        }
        copied_count += 1

    # 删除不再匹配的旧文件
    deleted_count = 0
    for old_src, old_entry in old_files.items():
        if old_src not in new_files:
            old_dst = data_dir / old_entry.get("dst_rel", "")
            if old_dst.exists():
                try:
                    old_dst.unlink()
                    deleted_count += 1
                except OSError as e:
                    logger.warning(f"删除旧镜像文件失败 {old_dst}: {e}")

    # 清理空目录
    _cleanup_empty_dirs(data_dir)

    # 保存 meta
    updated_meta = {
        "version": META_VERSION,
        "expr": expr,
        "raw_expr": raw_expr,
        "root": root,
        "pattern": pattern,
        "files": new_files,
    }
    _save_meta(meta_path, updated_meta)

    logger.info(
        f"Wildcard sync 完成: copied={copied_count}, "
        f"skipped={skipped_count}, deleted={deleted_count}, "
        f"total={len(new_files)}, mirror={data_dir}"
    )

    return data_dir


def _cleanup_empty_dirs(base: Path) -> None:
    """自底向上删除空目录。"""
    for dirpath, dirnames, filenames in os.walk(str(base), topdown=False):
        p = Path(dirpath)
        if p == base:
            continue
        if not any(p.iterdir()):
            try:
                p.rmdir()
            except OSError:
                pass
