from __future__ import annotations

import difflib
import os
import typing
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Callable

MARKER_BEGIN = "# >>> DOPETASK NEON BEGIN >>>"
MARKER_END = "# <<< DOPETASK NEON END <<<"

# Valid theme names for shell rc persistence (must match ui.THEMES keys)
VALID_THEMES = frozenset({"mintwave", "cyberpunk", "ultraviolet", "magma", "toxic_lime"})


def render_block(*, neon: str, theme: str, strict: str) -> str:
    if theme not in VALID_THEMES:
        raise ValueError(
            f"Unknown theme: {theme!r}. Valid themes: {', '.join(sorted(VALID_THEMES))}"
        )
    lines = [
        MARKER_BEGIN,
        f'export DOPETASK_NEON="{neon}"',
        f'export DOPETASK_THEME="{theme}"',
        f'export DOPETASK_STRICT="{strict}"',
        MARKER_END,
        "",
    ]
    return "\n".join(lines)


def apply_managed_block(contents: str, *, block: str, remove: bool) -> tuple[str, bool]:
    begin_idx = contents.find(MARKER_BEGIN)
    end_idx = contents.find(MARKER_END)

    # Reject files that contain multiple managed blocks, as behavior would be ambiguous.
    if begin_idx != -1:
        next_begin_idx = contents.find(MARKER_BEGIN, begin_idx + len(MARKER_BEGIN))
        if next_begin_idx != -1:
            raise ValueError("Multiple DOPETASK NEON begin markers found.")
    if end_idx != -1:
        next_end_idx = contents.find(MARKER_END, end_idx + len(MARKER_END))
        if next_end_idx != -1:
            raise ValueError("Multiple DOPETASK NEON end markers found.")
    if begin_idx == -1 and end_idx == -1:
        if remove:
            return contents, False
        prefix = "" if contents.endswith("\n") or contents == "" else "\n"
        return contents + prefix + block, True

    if begin_idx == -1 or end_idx == -1 or end_idx < begin_idx:
        if begin_idx == -1 and end_idx != -1:
            raise ValueError("Malformed DOPETASK NEON markers: begin marker missing.")
        if end_idx == -1 and begin_idx != -1:
            raise ValueError("Malformed DOPETASK NEON markers: end marker missing.")
        if end_idx < begin_idx:
            raise ValueError(
                "Malformed DOPETASK NEON markers: markers found in wrong order (end before begin)."
            )
        # Fallback for any unexpected mismatch.
        raise ValueError("Malformed DOPETASK NEON markers (begin/end mismatch).")

    end_idx = end_idx + len(MARKER_END)
    while end_idx < len(contents) and contents[end_idx] == "\n":
        end_idx += 1

    before = contents[:begin_idx]
    after = contents[end_idx:]
    if remove:
        # Remove the block and any immediate trailing newline(s) to avoid leaving gaps.
        new_contents = (before.rstrip("\n") + "\n" + after.lstrip("\n")).rstrip("\n") + "\n"
        return new_contents if new_contents != "\n" else "", True

    new_contents = before + block + after
    return new_contents, new_contents != contents


def unified_diff(old: str, new: str, *, path: Path) -> str:
    old_lines = old.splitlines(keepends=True)
    new_lines = new.splitlines(keepends=True)
    diff = difflib.unified_diff(
        old_lines,
        new_lines,
        fromfile=str(path),
        tofile=str(path),
    )
    return "".join(diff)


def _atomic_write(path: Path, content: str) -> None:
    import tempfile

    path.parent.mkdir(parents=True, exist_ok=True)
    try:
        with tempfile.NamedTemporaryFile(
            mode="w",
            encoding="utf-8",
            dir=path.parent,
            delete=False,
            prefix=f".{path.name}.",
            suffix=".dopetask.tmp",
        ) as tmp_file:
            tmp_file.write(content)
            tmp_path = Path(tmp_file.name)
        os.replace(tmp_path, path)
    except Exception:
        # Clean up temp file if it exists
        if "tmp_path" in locals() and tmp_path.exists():
            tmp_path.unlink()
        raise


def _default_backup_suffix() -> str:
    """Generate a timestamp-based backup suffix with microsecond precision.

    Uses format: YYYYMMDDHHMMSS_MMMMMM (e.g., 20260218074700_123456)
    This prevents backup file collisions when persist_rc_file is called
    multiple times within the same second.
    """
    import datetime
    now = datetime.datetime.now()
    return now.strftime("%Y%m%d%H%M%S") + f"_{now.microsecond:06d}"


@dataclass(frozen=True)
class PersistResult:
    path: Path
    changed: bool
    diff: str
    backup_path: typing.Optional[Path]


def persist_rc_file(
    *,
    path: Path,
    neon: str,
    theme: str,
    strict: str,
    remove: bool,
    dry_run: bool,
    backup_suffix_fn: Callable[[], str] = _default_backup_suffix,
) -> PersistResult:
    # Validate theme before any processing to prevent shell injection
    if theme not in VALID_THEMES:
        raise ValueError(
            f"Unknown theme: {theme!r}. Valid themes: {', '.join(sorted(VALID_THEMES))}"
        )

    # Read existing file content
    try:
        old = path.read_text(encoding="utf-8") if path.exists() else ""
    except (OSError, PermissionError) as exc:
        raise OSError(f"Failed to read rc file {path}: {exc}") from exc

    block = render_block(neon=neon, theme=theme, strict=strict)
    new, changed = apply_managed_block(old, block=block, remove=remove)
    diff = unified_diff(old, new, path=path)

    if dry_run:
        return PersistResult(path=path, changed=changed, diff=diff, backup_path=None)

    if not changed:
        return PersistResult(path=path, changed=False, diff=diff, backup_path=None)

    backup_path = path.with_name(f"{path.name}.dopetask.bak.{backup_suffix_fn()}")
    try:
        _atomic_write(backup_path, old)
    except (OSError, PermissionError) as exc:
        raise OSError(f"Failed to write backup file {backup_path}: {exc}") from exc

    try:
        _atomic_write(path, new)
    except (OSError, PermissionError) as exc:
        raise OSError(f"Failed to write rc file {path}: {exc}") from exc

    return PersistResult(path=path, changed=changed, diff=diff, backup_path=backup_path)
