from __future__ import annotations

import re
import shlex
import subprocess
from dataclasses import dataclass

from rapidfuzz import fuzz


FLAG_RE = re.compile(r"(?<![\w-])(--[a-zA-Z0-9][a-zA-Z0-9-]*|-[a-zA-Z0-9])(?![\w-])")


@dataclass(slots=True)
class VerificationResult:
    available_flags: list[str]
    suggested_flag: str | None
    corrected_command: str | None
    source: str


def parse_flags_from_text(text: str) -> list[str]:
    flags = set(FLAG_RE.findall(text))
    return sorted(flags)


def _run_help_command(command_name: str) -> tuple[str, str]:
    candidates = [
        ([command_name, "--help"], "help"),
        ([command_name, "-h"], "help"),
        (["man", command_name], "man"),
    ]
    for cmd, source in candidates:
        try:
            proc = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                check=False,
                timeout=2.0,
            )
        except (FileNotFoundError, subprocess.TimeoutExpired, OSError, PermissionError):
            continue

        output = "\n".join(part for part in (proc.stdout, proc.stderr) if part)
        if output.strip():
            return output, source
    return "", "none"


def suggest_flag(hallucinated_flag: str, available_flags: list[str]) -> str | None:
    if not hallucinated_flag or not available_flags:
        return None

    is_long_flag = hallucinated_flag.startswith("--")
    if is_long_flag:
        candidates = [flag for flag in available_flags if flag.startswith("--")]
    else:
        candidates = [flag for flag in available_flags if flag.startswith("-") and not flag.startswith("--")]

    if not candidates:
        return None

    normalized_bad = hallucinated_flag.lstrip("-")

    def score_candidate(candidate: str) -> float:
        normalized_candidate = candidate.lstrip("-")
        score = fuzz.ratio(normalized_bad, normalized_candidate)

        # Long flags need tighter confidence than short flags; avoid replacing a
        # typo with an unrelated but vaguely similar option.
        if is_long_flag:
            if not normalized_candidate or normalized_candidate[0] != normalized_bad[:1]:
                return -1.0
            if len(normalized_bad) > 2 and len(normalized_candidate) > 2:
                if normalized_candidate[:2] == normalized_bad[:2]:
                    score += 10
        return score

    scored_candidates = [(candidate, score_candidate(candidate)) for candidate in candidates]
    candidate, score = max(scored_candidates, key=lambda item: item[1], default=(None, -1.0))
    if candidate is None:
        return None
    threshold = 85 if is_long_flag else 70
    return candidate if score >= threshold else None


def corrected_command(command: str, bad_flag: str, good_flag: str) -> str | None:
    if not bad_flag or not good_flag:
        return None
    try:
        parts = shlex.split(command)
    except ValueError:
        return command.replace(bad_flag, good_flag, 1) if bad_flag in command else None

    for idx, token in enumerate(parts):
        if token == bad_flag:
            parts[idx] = good_flag
            return " ".join(shlex.quote(x) for x in parts)
    return None


def verify_command(command: str, bad_flag: str | None) -> VerificationResult:
    cmd_name = command.strip().split()[0] if command.strip() else ""
    if not cmd_name or cmd_name in {".", "source"}:
        return VerificationResult([], None, None, "none")

    help_text, source = _run_help_command(cmd_name)
    flags = parse_flags_from_text(help_text)
    if not bad_flag:
        return VerificationResult(flags, None, None, source)

    suggestion = suggest_flag(bad_flag, flags)
    fixed = corrected_command(command, bad_flag, suggestion) if suggestion else None
    return VerificationResult(flags, suggestion, fixed, source)
