"""Save outputs to a target directory."""

from __future__ import annotations

import shutil
from pathlib import Path
from typing import Any

from cascade_fm.core.cache.cache_none import NoCacheMixin
from cascade_fm.operations.base import Operation


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


class SaveTo(NoCacheMixin, Operation):
    """Copy input files to a user-selected target directory."""

    @property
    def name(self) -> str:
        return "save_to"

    @property
    def label(self) -> str:
        return "Save"

    @property
    def description(self) -> str:
        return "Save current output files into a destination folder"

    def _execute_impl(self, files: list[Path], **params: Any) -> list[Path]:
        target_raw = params["target_dir"]
        conflict_policy = str(params.get("conflict_policy", "skip")).lower()
        target_dir = Path(target_raw).expanduser()
        target_dir.mkdir(parents=True, exist_ok=True)

        planned_source_files = params.get("planned_source_files")
        planned_output_names = params.get("planned_output_names")

        if isinstance(planned_source_files, list) and isinstance(planned_output_names, list):
            return self._save_planned(
                target_dir=target_dir,
                conflict_policy=conflict_policy,
                planned_source_files=planned_source_files,
                planned_output_names=planned_output_names,
            )

        saved_paths: list[Path] = []
        total = len(files)
        for index, source in enumerate(files, start=1):
            self.report_progress(index - 1, total, f"Saving {source.name}")
            if not source.exists():
                continue

            destination = target_dir / source.name
            if destination.exists():
                if conflict_policy == "overwrite":
                    _remove_existing_destination(destination)
                elif conflict_policy == "skip":
                    continue
                elif conflict_policy == "fail":
                    raise FileExistsError(f"Destination exists: {destination}")
                else:
                    raise ValueError(f"Unsupported conflict policy: {conflict_policy}")

            if source.is_dir():
                shutil.copytree(source, destination)
            else:
                shutil.copy2(source, destination)
            saved_paths.append(destination)
            self.report_progress(index, total, f"Saved {destination.name}")

        if total > 0:
            self.report_progress(total, total, "Save complete")

        return saved_paths

    def _save_planned(
        self,
        *,
        target_dir: Path,
        conflict_policy: str,
        planned_source_files: list[Any],
        planned_output_names: list[Any],
    ) -> list[Path]:
        """Materialize virtual rename plans by copying sources to planned names."""
        saved_paths: list[Path] = []
        total = min(len(planned_source_files), len(planned_output_names))
        for index, (source_raw, name_raw) in enumerate(
            zip(planned_source_files, planned_output_names, strict=False),
            start=1,
        ):
            source = Path(str(source_raw)).expanduser()
            desired_name = Path(str(name_raw)).name
            self.report_progress(index - 1, total, f"Saving {desired_name or source.name}")
            if not source.exists() or not desired_name:
                continue

            destination = target_dir / desired_name
            if destination.exists():
                if conflict_policy == "overwrite":
                    _remove_existing_destination(destination)
                elif conflict_policy == "skip":
                    continue
                elif conflict_policy == "fail":
                    raise FileExistsError(f"Destination exists: {destination}")
                else:
                    raise ValueError(f"Unsupported conflict policy: {conflict_policy}")

            if source.is_dir():
                shutil.copytree(source, destination)
            else:
                shutil.copy2(source, destination)
            saved_paths.append(destination)
            self.report_progress(index, total, f"Saved {destination.name}")

        if total > 0:
            self.report_progress(total, total, "Save complete")

        return saved_paths

    def validate_params(self, **params: Any) -> tuple[bool, str | None]:
        target_dir = params.get("target_dir")
        if not isinstance(target_dir, str) or not target_dir.strip():
            return False, "target_dir must be a non-empty path"

        conflict_policy = params.get("conflict_policy", "skip")
        if not isinstance(conflict_policy, str):
            return False, "conflict_policy must be a string"
        if conflict_policy.lower() not in {"overwrite", "skip", "fail"}:
            return False, "conflict_policy must be one of: overwrite, skip, fail"

        planned_source_files = params.get("planned_source_files")
        planned_output_names = params.get("planned_output_names")
        if (planned_source_files is None) != (planned_output_names is None):
            return False, "planned_source_files and planned_output_names must be provided together"
        if planned_source_files is not None:
            if not isinstance(planned_source_files, list) or not all(
                isinstance(item, (str, Path)) for item in planned_source_files
            ):
                return False, "planned_source_files must be a list of paths"
            if not isinstance(planned_output_names, list) or not all(
                isinstance(item, (str, Path)) for item in planned_output_names
            ):
                return False, "planned_output_names must be a list of names"

        return True, None
