from __future__ import annotations

import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import List, Optional

import typer
from importlib.resources import files

from zsc import __version__ as ZSC_VERSION

# Where we persist the user's chosen skill language (under project root).
ZSC_LANG_FILE = ".agents/zsc-lang"
SUPPORTED_LANGS = ("en", "zh")


APP_HELP = (
    f"zsc {ZSC_VERSION}: 初始化并管理基于 .agents/tasks 的 AI Agents 任务体系。"
    "除 init 外，每个子命令对应一个 zsc-* 技能，由 zsc init 安装到各 AI Coding 工具的 skills 目录。"
)

app = typer.Typer(help=APP_HELP)


@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    upgrade: bool = typer.Option(
        False,
        "-U",
        "--upgrade",
        help="强制升级 zsc 包到最新版本（运行后退出）。/ Force upgrade zsc package to latest, then exit.",
    ),
    version: bool = typer.Option(
        False,
        "-V",
        "--version",
        help="显示当前 zsc 版本后退出。/ Show current zsc version and exit.",
    ),
) -> None:
    """
    zsc 是一个为多 IDE / AI Coding 工具初始化和管理 `.agents/tasks` 任务体系的命令行工具。
    """
    if version:
        typer.echo(f"zsc {ZSC_VERSION}")
        raise typer.Exit()

    if upgrade:
        _do_upgrade()
        raise typer.Exit()

    # 当没有指定子命令时，输出工具的整体目的和常用用法。
    if ctx.invoked_subcommand is None:
        typer.echo(
            f"zsc {ZSC_VERSION}: 初始化并管理基于 .agents/tasks 的 AI Agents 任务体系。\n\n"
            "常用命令（除 init 外，每个子命令对应一个 zsc-* 技能，由 zsc init 安装到各 AI Coding 工具的 skills 目录）：\n"
            "  zsc init .            在当前项目中创建 .agents 与任务/skills 目录\n"
            "  zsc task list         列出 .agents/tasks 下的任务及状态（技能: zsc-task-list）\n"
            "  zsc task new NAME     创建新的 task_{no}_{NAME} 任务目录（技能: zsc-create-task）\n"
            "  zsc task status       汇总任务状态（技能: zsc-task-status）\n"
            "  zsc task update [TASK]  更新某任务内容（技能: zsc-update-task）\n"
            "  （执行/推进任务请用技能: zsc-run-task；在安全环境中尽量一口气跑完整个任务，可考虑高级技能: zsc-run-task-to-complete）\n\n"
            "在 AI Coding 环境中可触发 zsc-help 技能（如 Cursor 中 /zsc-help）获取用法介绍。\n\n"
            "更多信息请见项目 README.md，或运行 `zsc --help` 查看完整命令列表。"
        )
        raise typer.Exit()


def _detect_broken_venv_python(base: Path) -> Optional[Path]:
    """
    Detect a broken virtualenv Python under .venv that is a dangling symlink.

    Returns the first broken interpreter path, or None if none found.
    """
    candidates = [
        base / ".venv" / "bin" / "python",
        base / ".venv" / "bin" / "python3",
        base / ".venv" / "Scripts" / "python.exe",
    ]
    for candidate in candidates:
        try:
            if candidate.is_symlink() and not candidate.exists():
                return candidate
        except OSError:
            # If the filesystem entry cannot be inspected, ignore and continue.
            continue
    return None


def _do_upgrade() -> None:
    """Force upgrade zsc package to latest via uv tool or pip, with env-aware fallbacks."""
    typer.echo("[zsc] Upgrading zsc to latest version...")

    base = Path.cwd()
    broken_venv_python = _detect_broken_venv_python(base)

    uv_path = shutil.which("uv")
    used_uv = False

    if uv_path and not broken_venv_python:
        cmd = ["uv", "tool", "install", "zsc", "--upgrade"]
        used_uv = True
    else:
        if uv_path and broken_venv_python:
            typer.echo(
                f"[zsc] Detected broken virtualenv interpreter at {broken_venv_python}. "
                "Skipping uv-based upgrade; consider recreating this environment with "
                "`uv venv` or your virtualenv tool.",
                err=True,
            )
        cmd = [sys.executable, "-m", "pip", "install", "-U", "zsc"]

    try:
        result = subprocess.run(cmd, check=False)
        if result.returncode != 0:
            if used_uv:
                # uv is available but failed; try a pip-based fallback before giving up.
                typer.echo(
                    "[zsc] uv-based upgrade failed, trying `pip install -U zsc` ...",
                    err=True,
                )
                fallback_cmd = [sys.executable, "-m", "pip", "install", "-U", "zsc"]
                fallback_result = subprocess.run(fallback_cmd, check=False)
                if fallback_result.returncode == 0:
                    typer.echo(
                        "[zsc] Upgrade finished via pip. Run `zsc` again to use the new version."
                    )
                    return

            typer.echo(
                "[zsc] Upgrade failed. You can try manually:\n"
                "  - uv tool install zsc --upgrade\n"
                "  - or: pip install -U zsc",
                err=True,
            )
            raise typer.Exit(1)

        typer.echo("[zsc] Upgrade finished. Run `zsc` again to use the new version.")
    except FileNotFoundError:
        typer.echo(
            "[zsc] Could not run upgrade command. "
            "Try: uv tool install zsc --upgrade or pip install -U zsc",
            err=True,
        )
        raise typer.Exit(1)


def _ensure_dir(path: Path) -> bool:
    """
    Ensure directory exists.

    Returns True if created, False if already existed.
    """
    if path.exists():
        return False
    path.mkdir(parents=True, exist_ok=True)
    return True


# Skills installed by `zsc init`. Each zsc command (except init) has a corresponding skill; zsc-help is the overview/help skill.
# zsc-run-task-to-complete is an advanced, high-automation execution skill built on top of zsc-run-task.
ZSC_SKILLS = [
    "zsc-help",
    "zsc-create-task",
    "zsc-run-task",
    "zsc-run-task-to-complete",
    "zsc-task-list",
    "zsc-task-status",
    "zsc-update-task",
]


def _normalize_lang(raw: str) -> str:
    """Return 'en' or 'zh' from user input or config."""
    v = raw.strip().lower()
    if v in ("en", "english", "2"):
        return "en"
    if v in ("zh", "chinese", "中文", "1", ""):
        return "zh"
    return "en"


def _get_init_lang(base: Path, lang_option: Optional[str]) -> str:
    """
    Determine language for skill prompts: from --lang, from .agents/zsc-lang, or interactive prompt.
    Returns 'en' or 'zh'.
    """
    lang_file = base / ZSC_LANG_FILE

    if lang_option is not None:
        lang = _normalize_lang(lang_option)
        return lang

    if lang_file.exists():
        try:
            return _normalize_lang(lang_file.read_text(encoding="utf-8"))
        except Exception:
            pass

    if not sys.stdin.isatty():
        return _normalize_lang(os.environ.get("ZSC_LANG", "en"))

    typer.echo(
        "[zsc] Skill 提示语言 / Language for skill prompts:\n"
        "  (1) 中文  (2) English\n"
        "  输入 1 或 2 选择 / Enter 1 or 2 (also accept: zh, en). 直接回车默认为中文 / Press Enter for 中文.\n"
    )
    try:
        raw = input("请输入 1 或 2 / Enter 1 or 2 [1]: ").strip() or "1"
    except (EOFError, KeyboardInterrupt):
        raw = "1"
    lang = _normalize_lang(raw)
    return lang


def _install_skill_if_missing(
    target_root: Path,
    tool_dir: str,
    skill_name: str,
    lang: str,
    created_messages: list[str],
) -> None:
    """
    Ensure the given zsc-* skill SKILL.md exists under the tool's skills directory.
    Uses SKILL.zh.md for Chinese (lang='zh'), SKILL.md for English.
    Overwrites existing SKILL.md so that re-running `zsc init` keeps all skills in the chosen language.
    """
    skills_root = target_root / tool_dir / "skills"
    created = _ensure_dir(skills_root)
    if created:
        created_messages.append(f"created directory: {skills_root}")

    target_skill_dir = skills_root / skill_name
    created_skill_dir = _ensure_dir(target_skill_dir)
    if created_skill_dir:
        created_messages.append(f"created directory: {target_skill_dir}")

    target_skill_file = target_skill_dir / "SKILL.md"
    resource_name = "SKILL.zh.md" if lang == "zh" else "SKILL.md"
    resource_path = files("zsc.resources").joinpath(skill_name, resource_name)
    if not resource_path.is_file():
        resource_path = files("zsc.resources").joinpath(skill_name, "SKILL.md")
    content = resource_path.read_text(encoding="utf-8")
    target_skill_file.write_text(content, encoding="utf-8")
    created_messages.append(f"wrote: {target_skill_file}")


def _tasks_root(base: Path) -> Path:
    agents_dir = base / ".agents"
    tasks_dir = agents_dir / "tasks"
    _ensure_dir(agents_dir)
    _ensure_dir(tasks_dir)
    return tasks_dir


def _iter_task_dirs(tasks_root: Path) -> List[Path]:
    if not tasks_root.exists():
        return []
    return sorted(
        [p for p in tasks_root.iterdir() if p.is_dir() and p.name.startswith("task_")],
        key=lambda p: p.name,
    )


def _resolve_task_dir(tasks_root: Path, identifier: str) -> Optional[Path]:
    """Resolve task identifier (e.g. '02', 'task_02', 'task_02_foo') to task directory, or None."""
    if not tasks_root.exists():
        return None
    ident = identifier.strip().lower()
    for d in _iter_task_dirs(tasks_root):
        if d.name == ident or d.name.lower() == ident:
            return d
        parts = d.name.split("_", 2)
        if ident.isdigit() and len(parts) >= 2 and parts[1] == ident.zfill(2):
            return d
        if ident in d.name or d.name.endswith("_" + ident):
            return d
    return None


def _task_md_path(task_dir: Path) -> Path:
    """
    Path to the task markdown file. Canonical name is task_dir/task_dir_name.md
    (e.g. task_03_init_lang_prompt.md) so AI Coding tools can reference by unique path.
    We never create task.md; fallback to task.md only when reading legacy tasks.
    """
    named_md = task_dir / f"{task_dir.name}.md"
    legacy_md = task_dir / "task.md"
    if named_md.exists():
        return named_md
    if legacy_md.exists():
        return legacy_md
    return named_md


def _parse_task_md(task_dir: Path) -> tuple[str, Optional[str]]:
    """
    Return (display_name, status) for a task directory.

    Status is a coarse-grained string: "completed" or "open".
    """
    task_md = _task_md_path(task_dir)
    if not task_md.exists():
        return (task_dir.name, None)

    text = task_md.read_text(encoding="utf-8").splitlines()

    title = task_dir.name
    for line in text:
        stripped = line.strip()
        if stripped.startswith("# Task"):
            title = stripped.lstrip("# ").strip()
            break

    # Heuristic: if we see completion marker and没有未完成的复选框，则认为已完成
    completed_marker = "（本轮已完成，TODO 清空）"
    has_completed_marker = any(completed_marker in line for line in text)
    has_open_todo = any(line.strip().startswith("- [ ]") for line in text)

    if has_completed_marker and not has_open_todo:
        status = "completed"
    elif has_open_todo:
        status = "open"
    else:
        status = None

    return (title, status)


@app.command("init")
def init(
    path: str = typer.Argument(
        ".",
        help="Target project root directory to initialize (default: current directory).",
    ),
    lang: Optional[str] = typer.Option(
        None,
        "--lang",
        help="Skill prompt language: zh (中文) or en (English). If not set, prompts interactively or uses existing .agents/zsc-lang.",
    ),
) -> None:
    """
    初始化项目中的 .agents 与任务目录，并将各 zsc-* 技能安装到 .cursor/.codex/.claudecode 的 skills 目录（仅当目标不存在时写入）。
    首次运行时会提示选择技能提示语言（中文/English）；也可通过 --lang zh 或 --lang en 指定。
    """
    base = Path(path).expanduser().resolve()

    if not base.exists():
        raise typer.BadParameter(f"Target path does not exist: {base}")

    typer.echo(f"[zsc] Initializing project at: {base}")

    created_messages: list[str] = []

    # Ensure .agents and .agents/tasks directories
    agents_dir = base / ".agents"
    tasks_dir = agents_dir / "tasks"

    if _ensure_dir(agents_dir):
        created_messages.append(f"created directory: {agents_dir}")
    else:
        created_messages.append(f"directory exists: {agents_dir}")

    if _ensure_dir(tasks_dir):
        created_messages.append(f"created directory: {tasks_dir}")
    else:
        created_messages.append(f"directory exists: {tasks_dir}")

    # Resolve skill language (prompt if needed) and persist
    chosen_lang = _get_init_lang(base, lang)
    if chosen_lang not in SUPPORTED_LANGS:
        chosen_lang = "en"
    lang_file = base / ZSC_LANG_FILE
    try:
        lang_file.write_text(chosen_lang + "\n", encoding="utf-8")
    except Exception:
        pass
    created_messages.append(f"skill language: {chosen_lang} ({'中文' if chosen_lang == 'zh' else 'English'})")

    # Install zsc-* skills for each AI Coding tool (one skill per zsc command, except init)
    for tool_dir in (".cursor", ".codex", ".claudecode"):
        for skill_name in ZSC_SKILLS:
            _install_skill_if_missing(base, tool_dir, skill_name, chosen_lang, created_messages)

    typer.echo("[zsc] Initialization summary:")
    for msg in created_messages:
        typer.echo(f"  - {msg}")

    typer.echo("[zsc] Done. You can now use AI Agents with the initialized task structure.")


task_app = typer.Typer(
    help="管理 .agents/tasks 下的任务。各子命令对应技能：list → zsc-task-list，new → zsc-create-task，status → zsc-task-status，update → zsc-update-task；执行任务 → zsc-run-task（由 zsc init 安装）。"
)


@task_app.command("list")
def task_list(
    path: str = typer.Argument(
        ".",
        help="Project root to scan for .agents/tasks.",
    ),
) -> None:
    """
    列出 .agents/tasks 下的任务及状态。对应技能：zsc-task-list（由 zsc init 安装）。
    """
    base = Path(path).expanduser().resolve()
    tasks_root = _tasks_root(base)
    task_dirs = _iter_task_dirs(tasks_root)

    if not task_dirs:
        typer.echo(f"[zsc] No tasks found under {tasks_root}")
        return

    typer.echo(f"[zsc] Tasks under {tasks_root}:")
    for task_dir in task_dirs:
        title, status = _parse_task_md(task_dir)
        status_display = status or "unknown"
        typer.echo(f"  - {task_dir.name:24} [{status_display}] {title}")


@task_app.command("new")
def task_new(
    feat_name: str = typer.Argument(
        ...,
        help="Feature name for the new task, used in directory name.",
    ),
    path: str = typer.Argument(
        ".",
        help="Project root where .agents/tasks resides.",
    ),
) -> None:
    """
    创建新的 task_{no}_{feat_name} 目录与同名的 task_{no}_{feat_name}.md 模板。对应技能：zsc-create-task（由 zsc init 安装）。创建后可在 AI Coding 工具中触发该技能完善闭环描述与 TODO_LIST。
    """
    base = Path(path).expanduser().resolve()
    tasks_root = _tasks_root(base)

    existing = _iter_task_dirs(tasks_root)
    max_no = 0
    for d in existing:
        parts = d.name.split("_", 2)
        if len(parts) >= 2 and parts[1].isdigit():
            try:
                num = int(parts[1])
            except ValueError:
                continue
            max_no = max(max_no, num)

    new_no = max_no + 1
    feat_slug = feat_name.strip().replace(" ", "_")
    task_dir_name = f"task_{new_no:02d}_{feat_slug}"
    task_dir = tasks_root / task_dir_name

    if task_dir.exists():
        raise typer.BadParameter(f"Task directory already exists: {task_dir}")

    _ensure_dir(task_dir)
    _ensure_dir(task_dir / "task_records" / "log")

    # Always use task_{no}_{feat}.md (same as directory name) for unique @ reference in AI tools
    task_md = task_dir / f"{task_dir_name}.md"
    task_md.write_text(
        f"# Task {new_no:02d}: {feat_name}\n\n"
        "## 闭环描述\n\n"
        "[在此填写该任务管理的目标资源及其完整生命周期（Create/Read/Update/Delete）的描述。]\n\n"
        "## TODO_LIST\n\n"
        "> 只维护最新版本；完成后清空 TODO，仅保留\"完成记录 + 日期\"。\n"
        "- （本轮已完成，TODO 清空）\n\n"
        "- [ ] 梳理与本任务目标资源最相关的现有代码与文档，在 task_records/log/ 下记录当前状态要点。\n"
        "- [ ] 根据当前状态，完善上方「闭环描述」中 Create/Read/Update/Delete 各小节的具体落点。\n"
        "- [ ] 将本任务拆解为 3–5 个可在 1–2 小时内完成的工程子任务，并用具体动作描述替换本示例 TODO。\n",
        encoding="utf-8",
    )

    typer.echo(f"[zsc] Created new task: {task_dir}")
    typer.echo(
        "[zsc] 下一步建议：\n"
        f"  1. 在你使用的 AI Coding 工具中打开 {task_md}。\n"
        "  2. 触发 zsc-create-task 技能（例如在 Cursor 中执行 /zsc-create-task）。\n"
        "  3. 将本次任务的需求意图作为输入，让 AI 帮你补全闭环描述与 TODO_LIST。\n\n"
        "当前版本的 zsc 仅负责创建目录与模板，不直接调用大模型；\n"
        "未来版本可能会内置 LLM 调用以自动完成这些步骤。"
    )


@task_app.command("status")
def task_status(
    path: str = typer.Argument(
        ".",
        help="Project root where .agents/tasks resides.",
    ),
) -> None:
    """
    汇总 .agents/tasks 中任务数量与状态。对应技能：zsc-task-status（由 zsc init 安装）。
    """
    base = Path(path).expanduser().resolve()
    tasks_root = _tasks_root(base)
    task_dirs = _iter_task_dirs(tasks_root)

    if not task_dirs:
        typer.echo(f"[zsc] No tasks found under {tasks_root}")
        return

    total = len(task_dirs)
    completed = 0
    open_tasks = 0
    unknown = 0

    for task_dir in task_dirs:
        _, status = _parse_task_md(task_dir)
        if status == "completed":
            completed += 1
        elif status == "open":
            open_tasks += 1
        else:
            unknown += 1

    typer.echo(f"[zsc] Task status summary for {tasks_root}:")
    typer.echo(f"  - total:     {total}")
    typer.echo(f"  - completed: {completed}")
    typer.echo(f"  - open:      {open_tasks}")
    typer.echo(f"  - unknown:   {unknown}")


@task_app.command("update")
def task_update(
    task: Optional[str] = typer.Argument(
        None,
        help="Task to update (e.g. task_02_zsc_task_cli, 02). If omitted, list tasks and suggest using zsc-update-task skill.",
    ),
    path: str = typer.Argument(
        ".",
        help="Project root where .agents/tasks resides.",
    ),
) -> None:
    """
    更新已有任务的内容（闭环描述、TODO_LIST 等）。仅编辑任务文件，不执行 TODO；对应技能：zsc-update-task（由 zsc init 安装）。执行任务请用 zsc-run-task。
    """
    base = Path(path).expanduser().resolve()
    tasks_root = _tasks_root(base)
    task_dirs = _iter_task_dirs(tasks_root)

    if not task_dirs:
        typer.echo(f"[zsc] No tasks found under {tasks_root}")
        typer.echo("[zsc] Use zsc-update-task skill in your AI Coding tool to update a task after creating one.")
        return

    if not task:
        typer.echo(f"[zsc] Tasks under {tasks_root} (use zsc task update TASK to open a task for update):")
        for task_dir in task_dirs:
            title, status = _parse_task_md(task_dir)
            status_display = status or "unknown"
            typer.echo(f"  - {task_dir.name:24} [{status_display}] {title}")
        typer.echo("\n[zsc] Or trigger zsc-update-task skill (e.g. /zsc-update-task in Cursor) to update a task.")
        return

    task_dir = _resolve_task_dir(tasks_root, task)
    if not task_dir:
        typer.echo(f"[zsc] No task matching '{task}' under {tasks_root}")
        typer.echo(f"[zsc] Available: {', '.join(d.name for d in task_dirs)}")
        raise typer.Exit(1)
    task_md = _task_md_path(task_dir)
    typer.echo(f"[zsc] Task to update: {task_dir.name}")
    typer.echo(f"[zsc] Task file: {task_md}")
    typer.echo("[zsc] Open this file and use zsc-update-task skill to revise 闭环描述, TODO_LIST, etc. Execution is via zsc-run-task.")


app.add_typer(task_app, name="task")


if __name__ == "__main__":
    app()

