import argparse
import csv
import json
import os
import sys
from typing import Iterable, List, Optional, Sequence, Tuple

try:
    import nuke
except ImportError as exc:
    raise RuntimeError('此脚本必须在 Nuke 的 Python 环境中运行。') from exc

try:
    from .node_utils import (
        apply_relinked_path,
        get_node_frame_range,
        get_node_name,
        is_range_mismatch,
    )
    from .scanner import collect_error_node_file_infos
    from .searcher import find_replacement_and_bounds
except ImportError:
    from node_utils import (  # type: ignore
        apply_relinked_path,
        get_node_frame_range,
        get_node_name,
        is_range_mismatch,
    )
    from scanner import collect_error_node_file_infos  # type: ignore
    from searcher import find_replacement_and_bounds  # type: ignore


DEFAULT_NODE_TYPES = 'Read,DeepRead,ReadGeo,Camera'
ResultRow = Tuple[object, str, str, Optional[str], Optional[int], Optional[int]]


def parse_extensions(text: Optional[str]) -> Optional[List[str]]:
    if not text:
        return None
    parts = [p.strip().lstrip('.').lower() for p in text.split(',') if p.strip()]
    return parts if parts else None


def _normalize_search_dirs(search_dirs: Iterable[str]) -> List[str]:
    normalized: List[str] = []
    seen = set()
    for path in search_dirs:
        expanded = os.path.abspath(os.path.expandvars(os.path.expanduser(str(path).strip())))
        if not os.path.isdir(expanded):
            continue
        key = expanded.lower() if os.name == 'nt' else expanded
        if key in seen:
            continue
        seen.add(key)
        normalized.append(expanded)
    return normalized


def _write_log(log_path: str, results: Sequence[ResultRow]) -> None:
    rows = []
    for node, knob_name, orig_path, new_path, first_frame, last_frame in results:
        original_first, original_last = get_node_frame_range(node)
        rows.append({
            'node': get_node_name(node),
            'knob': knob_name,
            'orig_path': orig_path,
            'new_path': new_path,
            'first_frame': first_frame,
            'last_frame': last_frame,
            'original_first_frame': original_first,
            'original_last_frame': original_last,
            'range_mismatch': is_range_mismatch(original_first, original_last, first_frame, last_frame),
        })

    directory = os.path.dirname(os.path.abspath(log_path))
    if directory:
        os.makedirs(directory, exist_ok=True)

    if log_path.lower().endswith('.json'):
        with open(log_path, 'w', encoding='utf-8') as handle:
            json.dump(rows, handle, ensure_ascii=False, indent=2)
        return

    with open(log_path, 'w', encoding='utf-8', newline='') as handle:
        writer = csv.DictWriter(
            handle,
            fieldnames=[
                'node',
                'knob',
                'orig_path',
                'new_path',
                'first_frame',
                'last_frame',
                'original_first_frame',
                'original_last_frame',
                'range_mismatch',
            ],
        )
        writer.writeheader()
        writer.writerows(rows)


def process_script(
    script_path: str,
    search_dirs: Iterable[str],
    node_types: Iterable[str],
    recursive: bool,
    extensions: Optional[List[str]] = None,
    replace: bool = False,
    use_relative: bool = False,
    save_out: Optional[str] = None,
    log_path: Optional[str] = None,
    allow_range_mismatch: bool = False,
) -> None:
    script_path = os.path.abspath(script_path)
    if not os.path.isfile(script_path):
        raise FileNotFoundError(f'未找到脚本文件：{script_path}')

    normalized_search_dirs = _normalize_search_dirs(search_dirs)
    if not normalized_search_dirs:
        raise ValueError('至少需要一个有效的搜索目录。')

    node_type_list = [str(item).strip() for item in node_types if str(item).strip()]
    if not node_type_list:
        raise ValueError('至少需要一个有效的节点类型。')

    nuke.scriptOpen(script_path)
    node_infos = collect_error_node_file_infos(node_type_list)
    results: List[ResultRow] = []
    for node, knob_name, orig_path in node_infos:
        try:
            new_path, first_frame, last_frame = find_replacement_and_bounds(
                orig_path,
                normalized_search_dirs,
                recursive,
                extensions,
            ) or (None, None, None)
        except Exception:
            new_path, first_frame, last_frame = (None, None, None)
        results.append((node, knob_name, orig_path, new_path, first_frame, last_frame))

    if replace and results:
        script_dir = os.path.dirname(script_path) if use_relative else None
        for node, knob_name, _orig_path, new_path, first_frame, last_frame in results:
            if not new_path:
                continue
            original_first, original_last = get_node_frame_range(node)
            if (not allow_range_mismatch) and is_range_mismatch(original_first, original_last, first_frame, last_frame):
                continue
            apply_relinked_path(
                node,
                knob_name,
                new_path,
                first_frame=first_frame,
                last_frame=last_frame,
                relative_base=script_dir,
            )

        out_path = os.path.abspath(save_out) if save_out else script_path
        out_dir = os.path.dirname(out_path)
        if out_dir:
            os.makedirs(out_dir, exist_ok=True)
        nuke.scriptSaveAs(out_path)

    if log_path:
        try:
            _write_log(log_path, results)
        except Exception as exc:
            print(f'日志导出失败: {exc}', file=sys.stderr)


def main(argv: Optional[List[str]] = None) -> None:
    parser = argparse.ArgumentParser(description='在命令行中查找并替换 Nuke 脚本中的丢失素材路径。')
    parser.add_argument('--script', required=True, help='要处理的 Nuke 脚本 (.nk) 路径')
    parser.add_argument('--search', nargs='+', required=True, help='一个或多个搜索目录')
    parser.add_argument('--node-types', default=DEFAULT_NODE_TYPES, help='逗号分隔的节点类型或节点家族')
    parser.add_argument('--recursive', action='store_true', help='递归搜索子目录')
    parser.add_argument('--extensions', default='', help='文件类型过滤，逗号分隔，例如 exr,png')
    parser.add_argument('--replace', action='store_true', help='是否执行路径替换')
    parser.add_argument('--relative', action='store_true', help='替换时将路径转换为相对路径')
    parser.add_argument('--save-out', help='替换后保存为新的脚本路径，缺省为覆盖原文件')
    parser.add_argument('--log', help='导出扫描结果的日志文件（CSV 或 JSON 格式）')
    parser.add_argument(
        '--allow-range-mismatch',
        action='store_true',
        help='允许在首尾帧范围不一致时仍然执行替换',
    )
    args = parser.parse_args(argv)

    node_types = [t.strip() for t in args.node_types.split(',') if t.strip()]
    extensions = parse_extensions(args.extensions)
    process_script(
        script_path=args.script,
        search_dirs=args.search,
        node_types=node_types,
        recursive=args.recursive,
        extensions=extensions,
        replace=args.replace,
        use_relative=args.relative,
        save_out=args.save_out,
        log_path=args.log,
        allow_range_mismatch=args.allow_range_mismatch,
    )


if __name__ == '__main__':
    main()
