"""Commit engine for writing pipeline results to the filesystem."""

from __future__ import annotations

import shutil
import tempfile
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

from cascade_fm.operations.base import CommitIntent


@dataclass
class CommitResult:
    """Summary of a commit run."""

    committed: int = 0
    failed: int = 0
    skipped: int = 0


class ConflictPolicy(Enum):
    """Policy for destination path conflicts during commit."""

    AUTO_RENAME = "auto_rename"
    OVERWRITE = "overwrite"
    SKIP = "skip"


def _unique_destination(target_dir: Path, desired_name: str) -> Path:
    """Build a non-conflicting destination path in target_dir."""
    destination = target_dir / desired_name
    if not destination.exists():
        return destination

    source_path = Path(desired_name)
    index = 1
    while True:
        candidate_name = f"{source_path.stem}_{index}{source_path.suffix}"
        candidate = target_dir / candidate_name
        if not candidate.exists():
            return candidate
        index += 1


def _resolve_destination(
    target_dir: Path,
    desired_name: str,
    conflict_policy: ConflictPolicy,
) -> Path | None:
    """Resolve destination path according to conflict policy."""
    destination = target_dir / desired_name

    if not destination.exists():
        return destination

    if conflict_policy is ConflictPolicy.OVERWRITE:
        return destination

    if conflict_policy is ConflictPolicy.SKIP:
        return None

    return _unique_destination(target_dir, desired_name)


def _remove_existing_destination(path: Path) -> None:
    """Remove existing destination path before overwrite."""
    if not path.exists():
        return
    if path.is_dir():
        shutil.rmtree(path)
    else:
        path.unlink()


def _atomic_copy_file(source: Path, destination: Path, overwrite: bool = False) -> None:
    """Copy a file to destination using a temporary file and atomic rename."""
    temp_file = tempfile.NamedTemporaryFile(
        prefix=f".{destination.name}.",
        suffix=".tmp",
        dir=destination.parent,
        delete=False,
    )
    temp_path = Path(temp_file.name)
    temp_file.close()

    try:
        shutil.copy2(source, temp_path)
        if overwrite:
            _remove_existing_destination(destination)
        temp_path.replace(destination)
    except Exception:
        if temp_path.exists():
            temp_path.unlink()
        raise


def _atomic_copy_directory(source: Path, destination: Path, overwrite: bool = False) -> None:
    """Copy a directory to destination using temporary directory and atomic rename."""
    temp_dir = Path(
        tempfile.mkdtemp(prefix=f".{destination.name}.", suffix=".tmp", dir=destination.parent)
    )

    try:
        staging_path = temp_dir / destination.name
        shutil.copytree(source, staging_path)
        if overwrite:
            _remove_existing_destination(destination)
        staging_path.replace(destination)
    except Exception:
        shutil.rmtree(temp_dir, ignore_errors=True)
        raise
    finally:
        if temp_dir.exists():
            shutil.rmtree(temp_dir, ignore_errors=True)


def _commit_materialized_outputs(
    output_files: list[Path],
    target_dir: Path,
    conflict_policy: ConflictPolicy,
) -> CommitResult:
    """Commit already-materialized operation outputs to target directory."""
    result = CommitResult()

    for source in output_files:
        if not source.exists():
            result.failed += 1
            continue

        destination = _resolve_destination(target_dir, source.name, conflict_policy)
        if destination is None:
            result.skipped += 1
            continue

        try:
            overwrite = conflict_policy is ConflictPolicy.OVERWRITE
            if source.is_dir():
                _atomic_copy_directory(source, destination, overwrite=overwrite)
            else:
                _atomic_copy_file(source, destination, overwrite=overwrite)
            result.committed += 1
        except OSError:
            result.failed += 1

    return result


def _commit_planned_paths(
    source_files: list[Path],
    planned_output_paths: list[Path],
    target_dir: Path,
    conflict_policy: ConflictPolicy,
) -> CommitResult:
    """Commit planned rename paths by copying source files to planned output names."""
    result = CommitResult()

    for source, planned_path in zip(source_files, planned_output_paths, strict=False):
        if not source.exists():
            result.failed += 1
            continue

        destination = _resolve_destination(target_dir, planned_path.name, conflict_policy)
        if destination is None:
            result.skipped += 1
            continue

        try:
            overwrite = conflict_policy is ConflictPolicy.OVERWRITE
            if source.is_dir():
                _atomic_copy_directory(source, destination, overwrite=overwrite)
            else:
                _atomic_copy_file(source, destination, overwrite=overwrite)
            result.committed += 1
        except OSError:
            result.failed += 1

    if len(source_files) != len(planned_output_paths):
        result.failed += abs(len(source_files) - len(planned_output_paths))

    return result


def commit_operation_output(
    commit_intent: CommitIntent,
    source_files: list[Path],
    output_files: list[Path],
    target_dir: Path,
    conflict_policy: ConflictPolicy = ConflictPolicy.AUTO_RENAME,
) -> CommitResult:
    """Commit output for an operation to target directory.

    Args:
        commit_intent: Typed intent describing how to commit this output.
        source_files: Input files for the operation.
        output_files: Output files for the operation.
        target_dir: Directory where committed results are written.
        conflict_policy: How to handle destination path conflicts.

    Returns:
        Commit summary with committed and failed counts.
    """
    if commit_intent is CommitIntent.PLANNED_OUTPUT_NAMES:
        return _commit_planned_paths(source_files, output_files, target_dir, conflict_policy)

    if commit_intent is CommitIntent.NONE:
        return CommitResult(committed=0, failed=len(output_files), skipped=0)

    return _commit_materialized_outputs(output_files, target_dir, conflict_policy)
