import os
import subprocess
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any, TypeAlias

Either = tuple
Printer: TypeAlias = Callable[[str], Any]
Ignore = lambda arg: None  # noqa


# todo this is protocol
@dataclass
class ShellConfig:
    color: bool
    ripgrep_max_columns: int


SC_DEFAULT = ShellConfig(color=False, ripgrep_max_columns=500)


def shell_to_text(cmd) -> tuple[Exception | None, str | None]:
    err, res = shell_to_lines(cmd)
    if err:
        return err, None
    return None, "\n".join(res or [])


def shell_to_lines(
    cmd, cwd=None, debug=False, cfg_shell: ShellConfig = SC_DEFAULT, timeout: int | None = None
) -> tuple[Exception | None, list[str] | None]:
    cwd = cwd or os.getcwd()
    # todo pass argument with_stderr
    # output_lines = _exec_subprocess_popen(cmd)
    if cmd.startswith("rg "):
        return _ripgrep_to_lines(cmd, cwd, debug, cfg_shell)
    try:
        output_lines = _exec_subprocess(cmd, cwd, timeout).strip().splitlines()
        return None, output_lines
    except subprocess.CalledProcessError as ex:
        ex_wrapped = SystemError(ex.stderr, ex)
        return ex_wrapped, None
    except subprocess.TimeoutExpired as ex:
        return SystemError(f"Command timed out after {timeout}s: {cmd}", ex), None


def _ripgrep_to_lines(cmd, cwd, debug, cfg_shell: ShellConfig) -> tuple[Exception | None, list[str] | None]:
    color_mode = "always" if cfg_shell.color else "never"

    cmd_suf = cmd[2:].strip()
    cmd = f"rg --vimgrep --color={color_mode}"
    if mc := cfg_shell.ripgrep_max_columns:
        cmd += f" --max-columns={mc}"

    cmd += f" {cmd_suf}"

    debug(f"Patched command to `{cmd}`")
    try:
        output_lines = _exec_subprocess(cmd, cwd).strip().splitlines()
        cwd = cwd or os.getcwd()
        res_lines = []
        for ln in output_lines:
            try:
                rpath, line_pos, col_pos, s_match = ln.split(":", 3)
                # todo fix: col_pos can be wrapped in colorized stuff
                col_pos = int(col_pos) - 1
                apath = rpath if cwd == "/" else os.path.join(cwd, rpath)
                ln_fix = f"{apath}:{line_pos}:{col_pos}:{s_match}"
            except ValueError:
                ln_fix = ln if cwd == "/" else os.path.join(cwd, ln)
            res_lines.append(ln_fix)
        res_lines.sort()
        return None, res_lines
    except subprocess.CalledProcessError as ex:
        if ex.returncode == 1:
            # when nothing found, ripgrep status code is `1` which is not problem
            return None, []
        return ex, None


def shell_raw(cmd):
    return _exec_os_system(cmd)


def _exec_os_system(cmd):
    return os.system(cmd)


def _exec_subprocess(cmd, cwd=None, timeout: int | None = None):
    result = subprocess.run(
        cmd,
        cwd=cwd,
        shell=True,
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        timeout=timeout,
    )
    if result.stderr:
        return (result.stdout + "\n" + result.stderr).strip()
    else:
        return result.stdout


def _exec_subprocess_popen(cmd):
    cmd_parts = cmd.split()
    lines = []
    err_lines = []
    process = None
    try:
        process = subprocess.Popen(
            cmd_parts,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True,
        )
        for line in process.stdout:
            lines.append(line.strip())
        for line in process.stderr:
            err_lines.append(line.strip())
    finally:
        process and process.kill()
    if err_lines:
        print("STDERR:")
        print("\n".join(err_lines))
    return lines


def remove_link(link_path: Path):
    assert link_path.is_symlink(), link_path
    target_path = Path(link_path).resolve()
    print(f"Removing {link_path} -> {target_path}")
    try:
        os.remove(link_path)
    except PermissionError:
        print(f"Need sudo to remove {link_path} -> {target_path}")
        shell_raw(f"sudo rm -f {link_path}")


def create_link(target_path: Path, link_path: Path):
    if link_path.is_symlink() and link_path.resolve() != target_path:
        remove_link(link_path)
    try:
        os.symlink(target_path, link_path)
    except PermissionError:
        print(f"Need sudo to create link {link_path} -> {target_path}")
        shell_raw(f"sudo ln -s {target_path} {link_path}")
