"""Subcommands: show, dump, init."""

from __future__ import annotations

import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from tacoshop.cli import ParseResult
    from tacoshop.config import Config, FlagVar, StaticVar


def _short_path(path_str: str) -> str:
    """Shorten a path for display: ./... for cwd-relative, ~/... for home."""
    path = Path(path_str).resolve()
    cwd = Path.cwd().resolve()
    home = Path.home().resolve()
    try:
        rel = path.relative_to(cwd)
        return f"./{rel}"
    except ValueError:
        pass
    # ~/... for anything under home (check before ../relative so home
    # doesn't show as ../../.tacoshop.toml)
    try:
        rel = path.relative_to(home)
        return f"~/{rel}"
    except ValueError:
        pass
    # Try ../relative for parent directories (e.g. git root above cwd)
    try:
        rel = os.path.relpath(path, cwd)
        if not rel.startswith("../../../"):  # don't go too far up
            return rel
    except ValueError:
        pass
    return path_str


def _compute_append(existing: str, new: str, separator: str) -> str:
    """Compute the result of appending a value (mirrors runner._apply_var)."""
    if separator and existing.endswith(separator):
        return existing + new
    elif separator:
        return existing + separator + new
    else:
        return existing + new


def _compute_prepend(existing: str, new: str, separator: str) -> str:
    """Compute the result of prepending a value (mirrors runner._apply_var)."""
    if separator and existing.startswith(separator):
        return new + existing
    elif separator:
        return new + separator + existing
    else:
        return new + existing


def _combine(existing: str, new: str, var: StaticVar | FlagVar) -> str:
    """Compute append or prepend result."""
    if var.append:
        return _compute_append(existing, new, var.separator)
    else:
        return _compute_prepend(existing, new, var.separator)


def _modifier_str(var: StaticVar | FlagVar) -> str:
    """Build the (append/prepend, sep=...) modifier string."""
    if not var.append and not var.prepend:
        return ""
    mode = "append" if var.append else "prepend"
    if var.separator:
        return f"  ({mode}, sep={var.separator!r})"
    return f"  ({mode})"


def _action_word(var: StaticVar | FlagVar) -> str:
    """Return 'appended' or 'prepended'."""
    return "appended" if var.append else "prepended"


def _get_var_def(name: str, fc: Config) -> StaticVar | FlagVar | None:
    """Get a variable's definition from a single-file config."""
    if name in fc.static_vars:
        return fc.static_vars[name]
    if name in fc.flag_vars:
        return fc.flag_vars[name]
    return None


def _print_static_trace(
    name: str, sv: StaticVar, config: Config, result: ParseResult
) -> None:
    """Print the trace for a static variable."""
    from tacoshop.config import FlagVar, StaticVar

    out = sys.stderr
    mod_str = _modifier_str(sv)
    desc_str = f"  # {sv.description}" if sv.description else ""
    print(f"  {name}{mod_str}{desc_str}", file=out)

    env_value = os.environ.get(name)
    running: str | None = None

    if env_value is not None:
        print(f"    environment            = {env_value!r}", file=out)
        running = env_value

    for fc in config.file_configs:
        var = _get_var_def(name, fc)
        if var is None:
            continue

        source = _short_path(var.source)

        # FlagVar entries in the static trace are conditional on the flag
        if isinstance(var, FlagVar):
            preset = var.value
            is_active = name in result.activated_flags

            if preset is None:
                # Value-required flag in static trace — just note it
                print(f"    {source}: defined {var.flag}", file=out)
                continue

            if not is_active:
                print(f"    {source}: if {var.flag} → {preset!r}", file=out)
                continue

            # Active flag with preset — treat like normal but annotate
            if (var.append or var.prepend) and running is not None:
                new_val = _combine(running, preset, var)
                print(f"    {source}: {_action_word(var)} {preset!r} → {new_val!r} ({var.flag})", file=out)
                running = new_val
            elif running is not None:
                print(f"    {source}: overwritten → {preset!r} ({var.flag})", file=out)
                running = preset
            else:
                print(f"    {source}: set → {preset!r} ({var.flag})", file=out)
                running = preset
            continue

        # StaticVar entries always apply
        value = var.value
        if (var.append or var.prepend) and running is not None:
            new_val = _combine(running, value, var)
            print(f"    {source}: {_action_word(var)} {value!r} → {new_val!r}", file=out)
            running = new_val
        elif running is not None:
            print(f"    {source}: overwritten → {value!r}", file=out)
            running = value
        else:
            print(f"    {source}: set → {value!r}", file=out)
            running = value

    print(file=out)


def _print_flag_trace(
    name: str, fv: FlagVar, config: Config, result: ParseResult
) -> None:
    """Print the trace for a flag-mapped variable."""
    from tacoshop.config import FlagVar, StaticVar

    out = sys.stderr
    is_active = name in result.activated_flags
    mod_str = _modifier_str(fv)
    desc_str = f"  # {fv.description}" if fv.description else ""
    print(f"  {name}{mod_str}{desc_str}", file=out)

    env_value = os.environ.get(name)
    running: str | None = None
    seen = False

    if env_value is not None:
        print(f"    environment            = {env_value!r}", file=out)
        running = env_value

    for fc in config.file_configs:
        var = _get_var_def(name, fc)
        if var is None:
            continue

        source = _short_path(var.source)

        if isinstance(var, FlagVar):
            preset = var.value

            if preset is not None:
                if is_active:
                    # Flag is active — show effect inline with flag annotation
                    if (var.append or var.prepend) and running is not None:
                        new_val = _combine(running, preset, var)
                        word = "overwritten, " if seen else ""
                        print(f"    {source}: {word}{_action_word(var)} {preset!r} → {new_val!r} ({fv.flag})", file=out)
                        running = new_val
                    elif running is not None:
                        print(f"    {source}: overwritten → {preset!r} ({fv.flag})", file=out)
                        running = preset
                    else:
                        print(f"    {source}: set → {preset!r} ({fv.flag})", file=out)
                        running = preset
                else:
                    # Flag is NOT active — show as conditional, don't update running
                    print(f"    {source}: if {var.flag} → {preset!r}", file=out)
            else:
                if seen:
                    print(f"    {source}: redefined {var.flag}", file=out)
                else:
                    print(f"    {source}: defined {var.flag}", file=out)
        elif isinstance(var, StaticVar):
            # Static entries always apply
            if (var.append or var.prepend) and running is not None:
                new_val = _combine(running, var.value, var)
                print(f"    {source}: {_action_word(var)} {var.value!r} → {new_val!r}", file=out)
                running = new_val
            elif running is not None:
                print(f"    {source}: overwritten → {var.value!r}", file=out)
                running = var.value
            else:
                print(f"    {source}: set → {var.value!r}", file=out)
                running = var.value

        seen = True

    # For active value-required flags, show the CLI-provided value
    if is_active and fv.value is None:
        cli_value = result.activated_flags[name]
        if (fv.append or fv.prepend) and running is not None:
            new_val = _combine(running, cli_value, fv)
            print(f"    {fv.flag}: {_action_word(fv)} {cli_value!r} → {new_val!r}", file=out)
        elif running is not None:
            print(f"    {fv.flag}: overwritten → {cli_value!r}", file=out)
        else:
            print(f"    {fv.flag}: set → {cli_value!r}", file=out)

    print(file=out)


def cmd_trace(config: Config, result: ParseResult) -> None:
    """Display all configured variables with load-order trace."""
    if not config.static_vars and not config.flag_vars:
        print("No configuration loaded.", file=sys.stderr)
        if not config.loaded_files:
            print(
                "No config files found. Create a tacoshop.toml or run 'tacoshop init'.",
                file=sys.stderr,
            )
        sys.exit(0)

    static_names = sorted(config.static_vars.keys())
    flag_names = sorted(
        config.flag_vars.keys(), key=lambda n: config.flag_vars[n].flag
    )

    if static_names:
        print("Static variables (always set):", file=sys.stderr)
        print(file=sys.stderr)
        for name in static_names:
            _print_static_trace(name, config.static_vars[name], config, result)

    if flag_names:
        print("Flag-mapped variables:", file=sys.stderr)
        print(file=sys.stderr)
        for name in flag_names:
            _print_flag_trace(name, config.flag_vars[name], config, result)

    if config.loaded_files:
        print("Loaded config files:", file=sys.stderr)
        for f in config.loaded_files:
            print(f"  {_short_path(f)}", file=sys.stderr)

    sys.exit(0)


def cmd_dumpenv(
    args: list[str], config: Config, result: ParseResult
) -> None:
    """Export environment to a sourceable shell script."""
    from tacoshop.runner import build_env

    # Parse dumpenv-specific args
    output_file: str | None = None
    i = 0
    while i < len(args):
        if args[i] in ("-o", "--output"):
            i += 1
            if i >= len(args):
                print("tacoshop dumpenv: -o requires a filename", file=sys.stderr)
                sys.exit(2)
            output_file = args[i]
        else:
            print(f"tacoshop dumpenv: unknown argument: {args[i]}", file=sys.stderr)
            sys.exit(2)
        i += 1

    env = build_env(config, result)

    # Collect only the vars that tacoshop manages
    managed: dict[str, str] = {}
    for sv in config.static_vars.values():
        managed[sv.name] = env.get(sv.name, sv.value)
    for env_name in result.activated_flags:
        managed[env_name] = env.get(env_name, result.activated_flags[env_name])

    lines: list[str] = []
    for name in sorted(managed):
        value = managed[name]
        # Escape single quotes for shell safety
        escaped = value.replace("'", "'\\''")
        lines.append(f"export {name}='{escaped}'")

    output = "\n".join(lines)
    if lines:
        output += "\n"

    if output_file:
        try:
            with open(output_file, "w") as f:
                f.write(output)
            print(f"tacoshop: wrote {output_file}", file=sys.stderr)
        except OSError as e:
            print(f"tacoshop dumpenv: {e}", file=sys.stderr)
            sys.exit(1)
    else:
        sys.stdout.write(output)

    sys.exit(0)


def _shell_quote(s: str) -> str:
    """Quote a string for shell use, only if needed."""
    import re

    if re.fullmatch(r'[A-Za-z0-9_./:=+@,-]+', s):
        return s
    return "'" + s.replace("'", "'\\''") + "'"


def cmd_print(
    args: list[str], config: Config, result: ParseResult
) -> None:
    """Print a one-liner with env var prefixes for copy-pasting."""
    from tacoshop.runner import build_env

    env = build_env(config, result)

    # Collect managed var names and their resolved values
    managed: dict[str, str] = {}
    for sv in config.static_vars.values():
        managed[sv.name] = env.get(sv.name, sv.value)
    for env_name in result.activated_flags:
        managed[env_name] = env.get(env_name, result.activated_flags[env_name])

    parts: list[str] = []
    for name in sorted(managed):
        parts.append(f"{name}={_shell_quote(managed[name])}")

    for token in args:
        parts.append(_shell_quote(token))

    print(" ".join(parts))
    sys.exit(0)


TEMPLATE = """\
# tacoshop.toml — environment variable configuration
#
# Static variables are always set when running through tacoshop.
# Just use top-level key = value pairs:

# TORCHINDUCTOR_FORCE_DISABLE_CACHES = 1
# MY_DEBUG_FLAG = "on"

# For a static variable that needs a description or append mode,
# use the table form (note: no "flag" field):
#
# [MY_STATIC_VAR]
# value = 42
# description = "Always-on setting"

# Flag-mapped variables are activated by a CLI flag.
# Use a [TABLE] with a "flag" field.

# Boolean toggle — flag activates a preset value:
#
# [TRITON_ALWAYS_COMPILE]
# flag = "--triton"
# value = 1
# description = "Force Triton to always recompile kernels"
#
# Usage: tacoshop --triton python train.py

# Value-required — user supplies the value at the CLI:
#
# [TORCH_LOGS]
# flag = "--logs"
# description = "Set torch logging channels (e.g. +dynamo)"
#
# Usage: tacoshop --logs "+dynamo" python train.py

# Append mode — append to an existing env var instead of replacing it.
# "separator" controls the join character (default: ""):
#
# [TORCH_LOGS]
# flag = "--logs"
# append = true
# separator = ","
# description = "Append to torch logging channels"
#
# If TORCH_LOGS is already "+inductor," in the shell, then
# tacoshop --logs "+dynamo," ... produces "+inductor,+dynamo,"

# Prepend mode — add the value to the front instead of the end:
#
# [PYTHONPATH]
# value = "/my/modules"
# prepend = true
# separator = ":"
# description = "Ensure my modules are found first"
#
# If PYTHONPATH is already "/other/stuff", the result is
# "/my/modules:/other/stuff"
"""


def cmd_init() -> None:
    """Create a template tacoshop.toml in the current directory."""
    for name in ("tacoshop.toml", ".tacoshop.toml"):
        if Path(name).exists():
            print(
                f"tacoshop: {name} already exists in this directory",
                file=sys.stderr,
            )
            sys.exit(1)

    Path("tacoshop.toml").write_text(TEMPLATE)
    print("tacoshop: created tacoshop.toml", file=sys.stderr)
    sys.exit(0)


def run_subcommand(
    name: str, args: list[str], config: Config, result: ParseResult
) -> None:
    """Dispatch to the appropriate subcommand."""
    if name == "trace":
        cmd_trace(config, result)
    elif name == "dumpenv":
        cmd_dumpenv(args, config, result)
    elif name == "print":
        cmd_print(args, config, result)
    elif name == "init":
        cmd_init()
    else:
        print(f"tacoshop: unknown subcommand: {name}", file=sys.stderr)
        sys.exit(2)
