"""Project script helpers for qpip."""

from __future__ import annotations

import json
import os
import re
import shlex
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Mapping, Optional, Sequence, TextIO

DEFAULT_QPIP_SCRIPTS = {
    "test": "python -m unittest discover -s tests -v",
}

TABLE_HEADER_PATTERN = re.compile(r"^\[(?P<name>[^\]]+)\]\s*$")
BARE_KEY_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$")
ENTRYPOINT_RUNNER_CODE = """
import importlib
import sys

script_name = sys.argv[1]
entrypoint = sys.argv[2]
extra_args = sys.argv[3:]
module_name, separator, qualname = entrypoint.partition(':')
if not separator or not module_name or not qualname:
    raise SystemExit(f'Invalid entry point: {entrypoint}')
obj = importlib.import_module(module_name)
for part in qualname.split('.'):
    obj = getattr(obj, part)
sys.argv = [script_name, *extra_args]
result = obj()
if isinstance(result, int):
    raise SystemExit(result)
raise SystemExit(0 if result is None else 0)
""".strip()

QPIP_SCRIPTS_TABLE = "tool.qpip.scripts"
PROJECT_SCRIPTS_TABLE = "project.scripts"
PROJECT_GUI_SCRIPTS_TABLE = "project.gui-scripts"
POETRY_SCRIPTS_TABLE = "tool.poetry.scripts"


class RunUsageError(ValueError):
    """Raised when qpip run usage is invalid."""


@dataclass(frozen=True)
class ScriptDefinition:
    """Definition of a script loaded from pyproject.toml."""

    name: str
    command: str
    source: str
    kind: str


def toml_string(value: str) -> str:
    """Render a Python string as a TOML basic string."""
    return json.dumps(value, ensure_ascii=False)


def format_toml_key(key: str) -> str:
    """Render a TOML key, quoting it when necessary."""
    return key if BARE_KEY_PATTERN.match(key) else toml_string(key)


def build_scripts_section(scripts: Mapping[str, str]) -> list[str]:
    """Build the qpip scripts section for pyproject.toml."""
    lines = [f"[{QPIP_SCRIPTS_TABLE}]"]
    for name, command in scripts.items():
        lines.append(f"{format_toml_key(name)} = {toml_string(command)}")
    return lines


def parse_toml_string(value: str) -> str:
    """Parse a one-line TOML string value."""
    raw = value.lstrip()
    if not raw:
        raise RunUsageError("scripts 配置值不能为空")

    if raw[0] == '"':
        escaped = False
        for index in range(1, len(raw)):
            char = raw[index]
            if escaped:
                escaped = False
                continue
            if char == "\\":
                escaped = True
                continue
            if char == '"':
                remainder = raw[index + 1 :].strip()
                if remainder and not remainder.startswith("#"):
                    raise RunUsageError("scripts 配置格式无效")
                return json.loads(raw[: index + 1])
        raise RunUsageError("scripts 字符串缺少结束引号")

    if raw[0] == "'":
        index = raw.find("'", 1)
        if index == -1:
            raise RunUsageError("scripts 字符串缺少结束引号")
        remainder = raw[index + 1 :].strip()
        if remainder and not remainder.startswith("#"):
            raise RunUsageError("scripts 配置格式无效")
        return raw[1:index]

    raise RunUsageError("scripts 仅支持单行字符串命令")


def parse_toml_key(value: str) -> str:
    """Parse a TOML key used in a script table."""
    raw = value.strip()
    if not raw:
        raise RunUsageError("scripts 配置键不能为空")
    if raw[0] in {'"', "'"}:
        return parse_toml_string(raw)
    return raw


def find_pyproject_path(cwd: Path) -> Path:
    """Return the pyproject.toml path in the working directory."""
    pyproject_path = cwd / "pyproject.toml"
    if not pyproject_path.exists():
        raise RunUsageError("当前目录未找到 pyproject.toml")
    return pyproject_path


def load_string_table(pyproject_path: Path, table_name: str) -> dict[str, str]:
    """Load a simple string table from pyproject.toml."""
    in_target_section = False
    found_target_section = False
    items: dict[str, str] = {}

    for line in pyproject_path.read_text(encoding="utf-8").splitlines():
        stripped = line.strip()
        table_match = TABLE_HEADER_PATTERN.match(stripped)
        if table_match:
            current_table_name = table_match.group("name").strip()
            if in_target_section:
                break
            if current_table_name == table_name:
                found_target_section = True
                in_target_section = True
            continue

        if not in_target_section:
            continue

        if not stripped or stripped.startswith("#"):
            continue

        key_part, separator, value_part = line.partition("=")
        if not separator:
            raise RunUsageError(f"[{table_name}] 中存在无效行")

        items[parse_toml_key(key_part)] = parse_toml_string(value_part)

    return items if found_target_section else {}


def load_project_scripts(pyproject_path: Path) -> dict[str, ScriptDefinition]:
    """Load compatible project scripts from pyproject.toml."""
    qpip_scripts = load_string_table(pyproject_path, QPIP_SCRIPTS_TABLE)
    if qpip_scripts:
        return {
            name: ScriptDefinition(name=name, command=command, source=QPIP_SCRIPTS_TABLE, kind="shell")
            for name, command in qpip_scripts.items()
        }

    fallback_tables = (
        (PROJECT_SCRIPTS_TABLE, "entrypoint"),
        (PROJECT_GUI_SCRIPTS_TABLE, "entrypoint"),
        (POETRY_SCRIPTS_TABLE, "entrypoint"),
    )
    scripts: dict[str, ScriptDefinition] = {}

    for table_name, kind in fallback_tables:
        for name, command in load_string_table(pyproject_path, table_name).items():
            scripts.setdefault(
                name,
                ScriptDefinition(name=name, command=command, source=table_name, kind=kind),
            )

    return scripts


def parse_run_args(args: Sequence[str]) -> tuple[Optional[str], tuple[str, ...]]:
    """Parse qpip run arguments."""
    if not args or args[0] != "run":
        raise RunUsageError("run 命令格式无效")

    trailing_args = list(args[1:])
    if "--" in trailing_args:
        separator_index = trailing_args.index("--")
        script_tokens = trailing_args[:separator_index]
        extra_args = tuple(trailing_args[separator_index + 1 :])
    else:
        script_tokens = trailing_args
        extra_args = ()

    if not script_tokens:
        return None, extra_args

    script_name = script_tokens[0]
    if len(script_tokens) > 1:
        raise RunUsageError("传递脚本参数请使用 `qpip run <script> -- <args>`")

    return script_name, extra_args


def format_shell_args(args: Sequence[str]) -> str:
    """Format extra shell arguments for display and execution."""
    if not args:
        return ""
    if os.name == "nt":
        return subprocess.list2cmdline(list(args))
    return shlex.join(list(args))


def build_run_display_command(definition: ScriptDefinition, extra_args: Sequence[str]) -> str:
    """Build a human-readable command preview for qpip run."""
    rendered_args = format_shell_args(extra_args)
    base = definition.command
    if not rendered_args:
        return base
    return f"{base} {rendered_args}"


def format_subprocess_command(command: Sequence[str]) -> str:
    """Render a subprocess command in a shell-friendly format."""
    if os.name == "nt":
        return subprocess.list2cmdline(list(command))
    return shlex.join(list(command))


def build_entrypoint_runner_command(
    definition: ScriptDefinition,
    extra_args: Sequence[str],
    python_executable: Optional[str] = None,
) -> list[str]:
    """Build the subprocess command used to execute an entry-point script."""
    return [
        python_executable or sys.executable,
        "-c",
        ENTRYPOINT_RUNNER_CODE,
        definition.name,
        definition.command,
        *extra_args,
    ]


def format_scripts(scripts: Mapping[str, ScriptDefinition]) -> str:
    """Format scripts for CLI output."""
    rows = ["可用脚本:"]
    for name in sorted(scripts):
        definition = scripts[name]
        if definition.source == QPIP_SCRIPTS_TABLE:
            rows.append(f"  {name:<12} {definition.command}")
        else:
            rows.append(f"  {name:<12} {definition.command} [{definition.source}]")
    return "\n".join(rows)


def missing_scripts_message() -> str:
    """Return the CLI message for projects without supported script tables."""
    return (
        f"当前项目未配置 [{QPIP_SCRIPTS_TABLE}]，"
        f"也未找到 [{PROJECT_SCRIPTS_TABLE}]、[{PROJECT_GUI_SCRIPTS_TABLE}] 或 [{POETRY_SCRIPTS_TABLE}]"
    )


def run_project_command(
    args: Sequence[str],
    *,
    cwd: Optional[Path] = None,
    output: Optional[TextIO] = None,
    dry_run: bool = False,
) -> int:
    """Handle `qpip run` by reading commands from pyproject.toml."""
    target_dir = Path.cwd() if cwd is None else cwd
    out = output if output is not None else os.sys.stdout
    pyproject_path = find_pyproject_path(target_dir)
    scripts = load_project_scripts(pyproject_path)
    script_name, extra_args = parse_run_args(args)

    if script_name is None:
        if not scripts:
            raise RunUsageError(missing_scripts_message())
        print(format_scripts(scripts), file=out)
        return 0

    if not scripts:
        raise RunUsageError(missing_scripts_message())

    try:
        definition = scripts[script_name]
    except KeyError as exc:
        available_scripts = ", ".join(sorted(scripts))
        raise RunUsageError(f"未找到脚本 '{script_name}'，可用脚本: {available_scripts}") from exc

    print(f"[qpip] {build_run_display_command(definition, extra_args)}", file=out)

    if dry_run:
        return 0

    if definition.kind == "shell":
        result = subprocess.run(
            build_run_display_command(definition, extra_args),
            shell=True,
            cwd=str(pyproject_path.parent),
            check=False,
        )
        return result.returncode

    command = build_entrypoint_runner_command(definition, extra_args)
    result = subprocess.run(command, cwd=str(pyproject_path.parent), check=False)
    return result.returncode