"""
Ticket 遷移命令模組

負責 Ticket ID 遷移功能，支援單一 Ticket 遷移和批量遷移。
"""
# 防止直接執行此模組
if __name__ == "__main__":
    from ticket_system.lib.messages import print_not_executable_and_exit
    print_not_executable_and_exit()



import argparse
import json
import re
import shutil
import yaml
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List

from ticket_system.lib.ui_constants import SEPARATOR_PRIMARY
from ticket_system.lib.constants import WORK_LOGS_DIR, TICKETS_DIR
from ticket_system.lib.ticket_loader import (
    get_project_root,
    get_ticket_path,
    get_tickets_dir,
    load_ticket,
    save_ticket,
    resolve_version,
)
from ticket_system.lib.parser import parse_frontmatter
from ticket_system.lib.ticket_validator import validate_ticket_id
from ticket_system.lib.id_parser import (
    extract_id_components,
    extract_core_ticket_id,
    parse_sequence,
    format_sequence,
    calculate_chain_info,
)
from ticket_system.lib.messages import (
    ErrorMessages,
    MigrationMessages,
    WarningMessages,
    InfoMessages,
    format_error,
    format_warning,
    format_info,
)
from ticket_system.lib.command_tracking_messages import (
    MigrateMessages,
)
from ticket_system.lib.ticket_ops import (
    load_and_validate_ticket,
)


def _update_ticket_id_references(ticket: Dict[str, Any], old_id: str, new_id: str) -> None:
    """
    更新 Ticket 中所有對舊 ID 的引用

    Args:
        ticket: Ticket 資料
        old_id: 舊 Ticket ID
        new_id: 新 Ticket ID
    """
    # 更新 blockedBy 引用（list of string）
    if "blockedBy" in ticket and ticket["blockedBy"]:
        ticket["blockedBy"] = [new_id if ref == old_id else ref for ref in ticket["blockedBy"]]

    # 更新 relatedTo 引用（list of string）
    if "relatedTo" in ticket and ticket["relatedTo"]:
        ticket["relatedTo"] = [new_id if ref == old_id else ref for ref in ticket["relatedTo"]]

    # 更新 spawned_tickets 引用（list of string）
    if "spawned_tickets" in ticket and ticket["spawned_tickets"]:
        ticket["spawned_tickets"] = [
            new_id if ref == old_id else ref for ref in ticket["spawned_tickets"]
        ]

    # 更新 children 中的 ID（同時支援 string 與 dict 兩種形式，與 _update_cross_references 一致）
    if "children" in ticket and ticket["children"]:
        new_children = []
        for child in ticket["children"]:
            if isinstance(child, str):
                new_children.append(new_id if child == old_id else child)
            elif isinstance(child, dict):
                if child.get("id") == old_id:
                    child["id"] = new_id
                new_children.append(child)
            else:
                new_children.append(child)
        ticket["children"] = new_children

    # 更新 source_ticket（scalar string）
    if "source_ticket" in ticket and ticket["source_ticket"] == old_id:
        ticket["source_ticket"] = new_id

    # 更新 parent_id（scalar string）
    if "parent_id" in ticket and ticket["parent_id"] == old_id:
        ticket["parent_id"] = new_id


def _update_cross_references(old_id: str, new_id: str) -> int:
    """
    搜尋所有 Ticket 文件並更新對舊 ID 的交叉引用

    掃描所有版本目錄下的 tickets 資料夾，查找並更新以下欄位中
    對舊 ID 的引用：
    - blockedBy: 阻塞依賴列表
    - relatedTo: 相關 Ticket 列表
    - children: 子 Ticket 列表（支援字串和 dict 形式）
    - source_ticket: 來源 Ticket
    - parent_id: 父 Ticket ID
    - spawned_tickets: 衍生 Ticket 列表

    Args:
        old_id: 舊 Ticket ID
        new_id: 新 Ticket ID

    Returns:
        int: 更新的檔案數量
    """
    updated_count = 0
    work_logs_root = get_project_root() / "docs" / "work-logs"

    # 掃描所有版本目錄下的 tickets 資料夾
    # 支援 flat (v{ver}/tickets) 與三層 (v{major}/v{major.minor}/v{ver}/tickets)
    flat_dirs = list(work_logs_root.glob("v*/tickets"))
    hierarchical_dirs = list(work_logs_root.glob("v*/v*/v*/tickets"))
    for tickets_dir in sorted(set(flat_dirs + hierarchical_dirs)):
        for ticket_file in sorted(tickets_dir.glob("*.md")):
            # 跳過剛遷移的 Ticket 本身（來源：0.18.0-W10-037 Bug 2）
            # 原本使用 startswith 會誤跳過子 Ticket（檔名以 new_id 開頭），
            # 導致它們的 blockedBy / parent_id / relatedTo 等引用不被更新。
            # 改用 extract_core_ticket_id 取得核心 ID 做精確比較，
            # 僅跳過「確實等於 new_id 的那個檔案」，子任務與帶後綴檔案都會被掃描。
            core_id = extract_core_ticket_id(ticket_file.stem)
            if core_id == new_id:
                continue

            # 載入 Ticket
            ticket = _load_ticket_from_path(ticket_file)
            if not ticket:
                continue

            # 檢查是否包含舊 ID 的引用
            updated = False

            # 更新 blockedBy
            if "blockedBy" in ticket and ticket.get("blockedBy"):
                if isinstance(ticket["blockedBy"], list):
                    for i, ref in enumerate(ticket["blockedBy"]):
                        if ref == old_id:
                            ticket["blockedBy"][i] = new_id
                            updated = True

            # 更新 relatedTo
            if "relatedTo" in ticket and ticket.get("relatedTo"):
                if isinstance(ticket["relatedTo"], list):
                    for i, ref in enumerate(ticket["relatedTo"]):
                        if ref == old_id:
                            ticket["relatedTo"][i] = new_id
                            updated = True

            # 更新 children（支援字串和 dict 形式）
            if "children" in ticket and ticket.get("children"):
                if isinstance(ticket["children"], list):
                    for i, child in enumerate(ticket["children"]):
                        if isinstance(child, str) and child == old_id:
                            ticket["children"][i] = new_id
                            updated = True
                        elif isinstance(child, dict) and child.get("id") == old_id:
                            child["id"] = new_id
                            updated = True

            # 更新 source_ticket
            if "source_ticket" in ticket and ticket.get("source_ticket") == old_id:
                ticket["source_ticket"] = new_id
                updated = True

            # 更新 parent_id
            if "parent_id" in ticket and ticket.get("parent_id") == old_id:
                ticket["parent_id"] = new_id
                updated = True

            # 更新 spawned_tickets
            if "spawned_tickets" in ticket and ticket.get("spawned_tickets"):
                if isinstance(ticket["spawned_tickets"], list):
                    for i, ref in enumerate(ticket["spawned_tickets"]):
                        if ref == old_id:
                            ticket["spawned_tickets"][i] = new_id
                            updated = True

            # 儲存修改
            if updated:
                try:
                    save_ticket(ticket, ticket_file)
                    updated_count += 1
                except (IOError, OSError) as e:
                    print(format_warning(
                        WarningMessages.FILE_UPDATE_FAILED,
                        path=str(ticket_file),
                        error=str(e)
                    ))

    return updated_count


def _load_ticket_from_path(ticket_path: Path) -> Optional[Dict[str, Any]]:
    """
    從檔案路徑載入 Ticket

    Args:
        ticket_path: Ticket 檔案路徑

    Returns:
        Dict: Ticket 資料，或 None 如果載入失敗
    """
    if not ticket_path.exists():
        return None

    try:
        with open(ticket_path, "r", encoding="utf-8") as f:
            content = f.read()
        # parse_frontmatter 返回 (frontmatter_dict, body_text)
        # W4-025：保留 body 至 ticket["_body"]，否則 save_ticket 寫回時
        # body 預設空字串 → 截斷被引用 ticket 的整個 Execution Log（資料損壞）
        ticket, body = parse_frontmatter(content)
        if ticket:
            ticket["_body"] = body
        return ticket if ticket else None
    except Exception:
        # 捕獲所有異常，包括 YAMLParseError 和其他解析錯誤
        # 無法載入的檔案將被跳過，不影響整個遷移流程
        return None


def _backup_ticket(version: str, ticket_id: str) -> Optional[Path]:
    """
    備份 Ticket 檔案

    Args:
        version: 版本號
        ticket_id: Ticket ID

    Returns:
        Path: 備份檔案路徑，或 None 如果備份失敗
    """
    root = get_project_root()
    backup_dir = root / ".claude" / "migration-backups" / datetime.now().strftime("%Y%m%d-%H%M%S")
    backup_dir.mkdir(parents=True, exist_ok=True)

    original_path = get_ticket_path(version, ticket_id)
    if not original_path.exists():
        return None

    backup_path = backup_dir / original_path.name
    try:
        shutil.copy2(original_path, backup_path)
        return backup_path
    except (IOError, OSError) as e:
        print(format_warning(WarningMessages.BACKUP_FAILED, error=str(e)))
        return None


def _check_target_collision(target_id: str, version: str) -> Optional[Dict[str, Any]]:
    """
    檢查目標 ID 是否與既有 Ticket 撞檔（W14-048 collision detection）。

    Args:
        target_id: 目標 Ticket ID
        version: fallback 版本號（若 target_id 無法解析版本時使用）

    Returns:
        Dict 包含 path/title/status；若無 collision 則回傳 None。
    """
    target_components = extract_id_components(target_id)
    target_version = target_components["version"] if target_components else version
    target_path = get_ticket_path(target_version, target_id)

    if not target_path.exists():
        return None

    existing = _load_ticket_from_path(target_path)
    return {
        "path": target_path,
        "title": (existing or {}).get("title", "N/A"),
        "status": (existing or {}).get("status", "N/A"),
    }


def _validate_target_version(target_id: str, version: str) -> Optional[str]:
    """
    驗證目標 Ticket ID 的版本是否已在 todolist.yaml 中註冊（W9-002）。

    僅阻擋完全未註冊的版本；planned/active/completed 皆放行，因為
    跨版本遷移（含遷入尚未 active 的未來版本）是正當場景。

    Args:
        target_id: 目標 Ticket ID
        version: fallback 版本號（若 target_id 無法解析版本時使用）

    Returns:
        Optional[str]: 未註冊時回傳錯誤訊息；已註冊（或無法判定）回傳 None
    """
    from ticket_system.lib.version import is_version_registered

    target_components = extract_id_components(target_id)
    target_version = target_components["version"] if target_components else version

    if is_version_registered(target_version):
        return None

    return ErrorMessages.VERSION_NOT_REGISTERED.format(version=target_version)


def _migrate_single_ticket(
    version: str,
    source_id: str,
    target_id: str,
    dry_run: bool = False,
    backup: bool = True,
    force_overwrite: bool = False,
) -> int:
    """
    遷移單一 Ticket

    Args:
        version: 版本號
        source_id: 來源 Ticket ID
        target_id: 目標 Ticket ID
        dry_run: 預覽模式
        backup: 是否備份
        force_overwrite: 明示授權覆寫目標 ID 既有 Ticket（W14-048）

    Returns:
        int: exit code (0 成功, 1 失敗, 2 來源 Ticket 不存在)
    """
    # 驗證 ID 格式
    if not validate_ticket_id(source_id) or not validate_ticket_id(target_id):
        print(MigrateMessages.INVALID_TICKET_ID_FORMAT)
        return 1

    # W9-002: 目標版本合法性守衛（未註冊阻擋；dry-run 同樣須過守衛）
    version_error = _validate_target_version(target_id, version)
    if version_error:
        print(format_error(version_error))
        return 1

    # 從 source_id 提取版本號，支援跨版本遷移
    source_components = extract_id_components(source_id)
    source_version = source_components["version"] if source_components else version

    # W14-048: 在 load_and_validate_ticket 之前先檢查實體檔案存在
    # （load_ticket 有 process-scoped cache，可能在檔案已刪除後仍命中快取，
    #  導致冪等性 re-run 場景誤判 source 仍存在）
    source_path_check = get_ticket_path(source_version, source_id)
    if not source_path_check.exists():
        return 2

    # 載入來源 Ticket
    ticket, error = load_and_validate_ticket(source_version, source_id)
    if error:
        return 2

    # W14-048: collision detection（target 已存在）
    # 例外：source == target（同 ID rename，等同 in-place 更新）不視為 collision
    collision = None
    if source_id != target_id:
        collision = _check_target_collision(target_id, version)

    # 預覽模式
    if dry_run:
        print(format_info(MigrateMessages.DRY_RUN_HEADER, source_id=source_id, target_id=target_id))
        print(f"{MigrateMessages.DRY_RUN_TITLE_PREFIX} {ticket.get('title', 'N/A')}")
        print(f"{MigrateMessages.DRY_RUN_STATUS_PREFIX} {ticket.get('status', 'N/A')}")
        if collision:
            print(format_warning(
                MigrateMessages.WARN_MIGRATE_TARGET_EXISTS,
                target_path=str(collision["path"]),
                existing_title=collision["title"],
                existing_status=collision["status"],
            ))
        return 0

    # W14-048: 實際執行階段，預設拒絕覆寫；--force-overwrite 旗標時記錄 audit log 繼續
    if collision:
        if not force_overwrite:
            print(format_error(
                MigrateMessages.ERROR_MIGRATE_TARGET_EXISTS,
                target_id=target_id,
                target_path=str(collision["path"]),
                existing_title=collision["title"],
                existing_status=collision["status"],
            ))
            return 1
        # force_overwrite=True：記錄 audit log 後繼續執行
        print(format_info(
            MigrateMessages.INFO_FORCE_OVERWRITE,
            target_id=target_id,
            timestamp=datetime.now().isoformat(),
            existing_title=collision["title"],
        ))

    # 執行備份
    backup_path = None
    if backup:
        backup_path = _backup_ticket(source_version, source_id)
        if backup_path:
            print(format_info(InfoMessages.FILE_BACKED_UP, path=str(backup_path)))
        else:
            print(format_warning(WarningMessages.BACKUP_SKIPPED))

    # 更新 Ticket 資料
    old_id = ticket.get("id")
    ticket["id"] = target_id

    # 更新 wave
    components = extract_id_components(target_id)
    if components:
        ticket["version"] = components["version"]
        ticket["wave"] = components["wave"]

        # 更新 chain 資訊
        chain_info = calculate_chain_info(target_id)
        if chain_info:
            ticket["chain"] = chain_info

        # 更新 parent_id
        if chain_info.get("parent"):
            ticket["parent_id"] = chain_info["parent"]

    # 更新所有引用
    _update_ticket_id_references(ticket, old_id, target_id)

    # 取得原始檔案路徑
    source_path = get_ticket_path(source_version, source_id)

    # 提取目標版本（從目標 ID 中）以支援跨版本遷移
    target_components = extract_id_components(target_id)
    target_version = target_components["version"] if target_components else version
    target_path = get_ticket_path(target_version, target_id)

    # 確保目錄存在
    target_path.parent.mkdir(parents=True, exist_ok=True)

    try:
        # 儲存到新位置
        save_ticket(ticket, target_path)
        print(format_info(InfoMessages.TICKET_MIGRATED, source_id=source_id, target_id=target_id))

        # 刪除舊檔案
        if source_path != target_path and source_path.exists():
            source_path.unlink()
            print(format_info(InfoMessages.FILE_DELETED, path=str(source_path)))

        # 更新其他 Ticket 中的交叉引用
        cross_ref_count = _update_cross_references(old_id, target_id)
        if cross_ref_count > 0:
            print(format_info(MigrateMessages.CROSS_REFERENCES_UPDATED, count=cross_ref_count))

        return 0

    except (IOError, OSError) as e:
        print(format_error(ErrorMessages.FILE_CREATION_FAILED, error=str(e)))
        if backup_path:
            print(format_info(MigrationMessages.BACKUP_LOCATION, path=str(backup_path)))
        return 1


def _load_migration_config(config_file: str) -> Optional[List[Dict[str, str]]]:
    """
    載入遷移配置檔案

    Args:
        config_file: 配置檔案路徑

    Returns:
        List[Dict]: 遷移清單，或 None 如果載入失敗
    """
    config_path = Path(config_file)

    if not config_path.exists():
        print(f"{MigrateMessages.CONFIG_FILE_NOT_FOUND} {config_file}")
        return None

    try:
        with open(config_path, "r", encoding="utf-8") as f:
            if config_file.endswith(".yaml") or config_file.endswith(".yml"):
                data = yaml.safe_load(f)
            elif config_file.endswith(".json"):
                data = json.load(f)
            else:
                print(MigrateMessages.CONFIG_FORMAT_NOT_SUPPORTED)
                return None

        if not isinstance(data, dict) or "migrations" not in data:
            print(MigrateMessages.CONFIG_FORMAT_INVALID)
            return None

        migrations = data["migrations"]
        if not isinstance(migrations, list):
            print(MigrateMessages.MIGRATIONS_FIELD_NOT_LIST)
            return None

        return migrations

    except (IOError, OSError, yaml.YAMLError, json.JSONDecodeError) as e:
        print(f"{MigrateMessages.CONFIG_LOAD_FAILED} {e}")
        return None


def _batch_migrate(
    version: str,
    config_file: str,
    dry_run: bool = False,
    backup: bool = True,
    force_overwrite: bool = False,
) -> int:
    """
    批量遷移 Tickets

    Args:
        version: 版本號
        config_file: 配置檔案路徑
        dry_run: 預覽模式
        backup: 是否備份
        force_overwrite: 明示授權覆寫目標 ID 既有 Ticket（W14-048）

    Returns:
        int: exit code (0 全部成功, 1 部分失敗, 2 全部失敗)
    """
    migrations = _load_migration_config(config_file)
    if not migrations:
        return 1

    print(format_info(MigrationMessages.LOAD_MIGRATIONS, count=len(migrations)))

    # W14-048: 預掃描所有目標 ID 的 collision，任一撞 ID 即 fail-fast 不執行任何 migration
    # 例外：
    # - force_overwrite=True 時跳過 pre-scan，由個別 _migrate_single_ticket 記錄 audit log
    # - source 不存在時跳過（_migrate_single_ticket 會 return 2 並由 skip_count 計入），
    #   讓 idempotent re-run 不會誤判 collision（test_w11_reorganization_idempotency）
    if not dry_run and not force_overwrite:
        collisions = []
        for migration in migrations:
            if not isinstance(migration, dict):
                continue
            source_id = migration.get("from")
            target_id = migration.get("to")
            if not source_id or not target_id:
                continue
            # source == target 不視為 collision（in-place rename）
            if source_id == target_id:
                continue
            # source 不存在則交由個別執行 skip，不在 pre-scan 算 collision
            source_components = extract_id_components(source_id)
            source_version = source_components["version"] if source_components else version
            source_path = get_ticket_path(source_version, source_id)
            if not source_path.exists():
                continue
            collision = _check_target_collision(target_id, version)
            if collision:
                collisions.append(
                    f"  - {source_id} → {target_id}（既有: {collision['title']} / {collision['status']}）"
                )
        if collisions:
            print(format_error(
                MigrateMessages.ERROR_BATCH_COLLISION,
                collisions="\n".join(collisions),
            ))
            return 1

    success_count = 0
    fail_count = 0
    skip_count = 0

    for migration in migrations:
        if not isinstance(migration, dict):
            print(format_warning(WarningMessages.INVALID_MIGRATION_ITEM))
            skip_count += 1
            continue

        source_id = migration.get("from")
        target_id = migration.get("to")

        if not source_id or not target_id:
            print(format_warning(WarningMessages.MIGRATION_ITEM_INCOMPLETE))
            skip_count += 1
            continue

        print()
        result = _migrate_single_ticket(
            version, source_id, target_id, dry_run, backup, force_overwrite
        )

        if result == 0:
            success_count += 1
        elif result == 2:
            skip_count += 1
        else:
            fail_count += 1

    # 輸出摘要
    print()
    print(SEPARATOR_PRIMARY)
    print(format_info(MigrateMessages.MIGRATION_SUMMARY))
    print(format_info(MigrationMessages.SUCCESS_COUNT, count=success_count))
    print(format_info(MigrationMessages.FAIL_COUNT, count=fail_count))
    print(format_info(MigrationMessages.SKIP_COUNT, count=skip_count))
    print(SEPARATOR_PRIMARY)

    if fail_count > 0:
        return 1 if success_count > 0 else 2
    return 0


def execute(args: argparse.Namespace) -> int:
    """執行 migrate 命令"""
    # 使用共用 API 解析版本（自動標準化移除 v 前綴）
    version = resolve_version(getattr(args, "version", None))
    if not version:
        print(format_error(ErrorMessages.VERSION_NOT_DETECTED))
        return 1

    # 檢查是否為批量遷移
    if getattr(args, "config", None):
        dry_run = getattr(args, "dry_run", False)
        backup = not getattr(args, "no_backup", False)
        force_overwrite = getattr(args, "force_overwrite", False)
        return _batch_migrate(version, args.config, dry_run, backup, force_overwrite)

    # 單一 Ticket 遷移
    source_id = getattr(args, "source_id", None)
    target_id = getattr(args, "target_id", None)

    if not source_id or not target_id:
        print(format_error(ErrorMessages.MISSING_PARAMETERS))
        return 1

    dry_run = getattr(args, "dry_run", False)
    backup = not getattr(args, "no_backup", False)
    force_overwrite = getattr(args, "force_overwrite", False)

    return _migrate_single_ticket(
        version, source_id, target_id, dry_run, backup, force_overwrite
    )


def register(subparsers: argparse._SubParsersAction) -> None:
    """註冊 migrate 子命令"""
    parser = subparsers.add_parser(
        "migrate",
        help=MigrateMessages.HELP_MIGRATE
    )

    # 位置參數（用於單一遷移）
    parser.add_argument(
        "source_id",
        nargs="?",
        help=MigrateMessages.ARG_SOURCE_ID
    )
    parser.add_argument(
        "target_id",
        nargs="?",
        help=MigrateMessages.ARG_TARGET_ID
    )

    # 選項
    parser.add_argument(
        "--config",
        metavar="FILE",
        help=MigrateMessages.ARG_CONFIG
    )
    parser.add_argument(
        "--version",
        help=MigrateMessages.ARG_VERSION
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help=MigrateMessages.ARG_DRY_RUN
    )
    parser.add_argument(
        "--backup",
        action="store_true",
        default=True,
        help=MigrateMessages.ARG_BACKUP
    )
    parser.add_argument(
        "--no-backup",
        action="store_true",
        help=MigrateMessages.ARG_NO_BACKUP
    )
    parser.add_argument(
        "--force-overwrite",
        action="store_true",
        default=False,
        help=MigrateMessages.ARG_FORCE_OVERWRITE
    )

    parser.set_defaults(func=execute)


