"""Code formatting utilities (import sorting, external formatters)."""

from __future__ import annotations

import difflib
import logging
import os
import re
import shlex
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any

import isort

log = logging.getLogger(__name__)

KNOWN_FIRST_PARTY = ["codeflash_python", "codeflash_core"]


def sort_imports(
    code: str, *, float_to_top: bool = True, **kwargs: Any
) -> str:
    """Sort and deduplicate imports in *code* using isort."""
    try:
        sorted_code = isort.code(
            code,
            known_first_party=KNOWN_FIRST_PARTY,
            float_to_top=float_to_top,
            **kwargs,
        )
    except Exception:
        log.exception("Failed to sort imports with isort.")
        return code
    return sorted_code


def generate_unified_diff(
    original: str, modified: str, from_file: str, to_file: str
) -> str:
    """Return a unified diff between *original* and *modified*."""
    line_pattern = re.compile(r"(.*?(?:\r\n|\n|\r|$))")

    def split_lines(text: str) -> list[str]:
        lines = [match[0] for match in line_pattern.finditer(text)]
        if lines and lines[-1] == "":
            lines.pop()
        return lines

    original_lines = split_lines(original)
    modified_lines = split_lines(modified)

    diff_output: list[str] = []
    for line in difflib.unified_diff(
        original_lines,
        modified_lines,
        fromfile=from_file,
        tofile=to_file,
        n=5,
    ):
        if line.endswith("\n"):
            diff_output.append(line)
        else:
            diff_output.append(line + "\n")
            diff_output.append("\\ No newline at end of file\n")

    return "".join(diff_output)


def get_diff_lines_count(diff_output: str) -> int:
    """Count changed lines (additions/deletions) in a unified diff."""
    lines = diff_output.split("\n")

    def is_diff_line(line: str) -> bool:
        return line.startswith(("+", "-")) and not line.startswith(
            ("+++", "---")
        )

    diff_lines = [line for line in lines if is_diff_line(line)]
    return len(diff_lines)


def apply_formatter_cmds(
    cmds: list[str],
    path: Path,
    test_dir_str: str | None,
    print_status: bool,
    exit_on_failure: bool = True,
) -> tuple[Path, str, bool]:
    """Run a sequence of formatter shell commands against *path*."""
    if not path.exists():
        msg = f"File {path} does not exist. Cannot apply formatter commands."
        raise FileNotFoundError(msg)

    file_path = path
    if test_dir_str:
        file_path = Path(test_dir_str) / "temp.py"
        shutil.copy2(path, file_path)

    file_token = "$file"  # noqa: S105

    changed = False
    for command in cmds:
        formatter_cmd_list = shlex.split(command, posix=os.name != "nt")
        formatter_cmd_list = [
            file_path.as_posix() if chunk == file_token else chunk
            for chunk in formatter_cmd_list
        ]
        try:
            result = subprocess.run(  # noqa: S603
                formatter_cmd_list, capture_output=True, check=False
            )
            if result.returncode == 0:
                if print_status:
                    log.info(
                        "Formatted successfully with: %s",
                        command.replace("$file", path.name),
                    )
                changed = True
            else:
                log.error(
                    "Failed to format code with %s",
                    " ".join(formatter_cmd_list),
                )
        except FileNotFoundError as e:
            command_str = " ".join(str(part) for part in formatter_cmd_list)
            log.warning("Formatter command not found: %s", command_str)
            if exit_on_failure:
                raise e from None

    return file_path, file_path.read_text(encoding="utf8"), changed


def format_generated_code(
    generated_test_source: str,
    formatter_cmds: list[str],
    language: str = "python",
) -> str:
    """Format *generated_test_source* using external formatter commands."""
    formatter_name = (
        formatter_cmds[0].lower() if formatter_cmds else "disabled"
    )
    if formatter_name == "disabled":
        return re.sub(r"\n{2,}", "\n\n", generated_test_source)

    with tempfile.TemporaryDirectory() as test_dir_str:
        ext = _extension_for_language(language)
        original_temp = Path(test_dir_str) / ("original_temp" + ext)
        original_temp.write_text(generated_test_source, encoding="utf8")
        _, formatted_code, changed = apply_formatter_cmds(
            formatter_cmds,
            original_temp,
            test_dir_str,
            print_status=False,
            exit_on_failure=False,
        )
        if not changed:
            return re.sub(r"\n{2,}", "\n\n", formatted_code)
    return formatted_code


def format_code(
    formatter_cmds: list[str],
    path: str | Path,
    optimized_code: str = "",
    check_diff: bool = False,
    print_status: bool = True,
    exit_on_failure: bool = True,
) -> str | None:
    """Run external formatter commands on the file at *path*."""
    if isinstance(path, str):
        path = Path(path)

    formatter_name = (
        formatter_cmds[0].lower() if formatter_cmds else "disabled"
    )
    if formatter_name == "disabled":
        return path.read_text(encoding="utf8")

    with tempfile.TemporaryDirectory() as test_dir_str:
        original_code = path.read_text(encoding="utf8")
        original_code_lines = len(original_code.split("\n"))

        if check_diff and original_code_lines > 50:
            original_code_without_opfunc = original_code.replace(
                optimized_code, ""
            )

            original_temp = Path(test_dir_str) / "original_temp.py"
            original_temp.write_text(
                original_code_without_opfunc, encoding="utf8"
            )

            formatted_temp, formatted_code, changed = apply_formatter_cmds(
                formatter_cmds,
                original_temp,
                test_dir_str,
                print_status=False,
                exit_on_failure=exit_on_failure,
            )

            if not changed:
                log.warning(
                    "No changes detected in %s after formatting, are you"
                    " sure you have valid formatter commands?",
                    path,
                )
                return original_code

            diff_output = generate_unified_diff(
                original_code_without_opfunc,
                formatted_code,
                from_file=str(original_temp),
                to_file=str(formatted_temp),
            )
            diff_lines_count = get_diff_lines_count(diff_output)

            max_diff_lines = min(int(original_code_lines * 0.3), 50)

            if diff_lines_count > max_diff_lines:
                log.warning(
                    "Skipping formatting %s: %d lines would change (max: %d)",
                    path,
                    diff_lines_count,
                    max_diff_lines,
                )
                return original_code

        _, formatted_code, changed = apply_formatter_cmds(
            formatter_cmds,
            path,
            test_dir_str=None,
            print_status=print_status,
            exit_on_failure=exit_on_failure,
        )

        if not changed:
            log.warning(
                "No changes detected in %s after formatting, are you"
                " sure you have valid formatter commands?",
                path,
            )
            return original_code

        log.debug("Formatted %s with commands: %s", path, formatter_cmds)
        return formatted_code


def _extension_for_language(language: str) -> str:
    """Map a language name to a file extension."""
    extensions: dict[str, str] = {
        "python": ".py",
        "javascript": ".js",
        "typescript": ".ts",
        "java": ".java",
    }
    return extensions.get(language, ".py")
