from __future__ import annotations

import os
from pathlib import Path

import uuid

from backend.app.api.errors import AppError
from backend.app.api.schemas import TaskCreateResponse
from backend.app.db.history_repository import HistoryRepository
from backend.app.db.task_repository import TaskRepository
from backend.app.security.path_guard import PathGuard, ResolvedPath
from backend.app.tasks_runner import TaskRunner


class CopyTaskService:
    def __init__(self, path_guard: PathGuard, repository: TaskRepository, runner: TaskRunner, history_repository: HistoryRepository | None = None):
        self._path_guard = path_guard
        self._repository = repository
        self._runner = runner
        self._history_repository = history_repository

    def create_copy_task(self, source: str, destination: str) -> TaskCreateResponse:
        if not source or not destination:
            raise AppError(
                code="invalid_request",
                message="Source and destination are required",
                status_code=400,
            )
        try:
            item = self._build_copy_item(source=source, destination=destination)
            task_id = str(uuid.uuid4())
            task = self._repository.create_task(
                operation="copy",
                source=item["source_relative"],
                destination=item["destination_relative"],
                task_id=task_id,
            )
            self._record_history(
                entry_id=task_id,
                operation="copy",
                status="queued",
                source=item["source_relative"],
                destination=item["destination_relative"],
            )

            if item["kind"] == "directory":
                self._runner.enqueue_copy_directory(task_id=task["id"], item=item)
            else:
                self._runner.enqueue_copy_file(
                    task_id=task["id"],
                    source=item["source_absolute"],
                    destination=item["destination_absolute"],
                    total_bytes=item["total_bytes"],
                    current_item=item["files"][0]["label"],
                )

            return TaskCreateResponse(task_id=task["id"], status=task["status"])
        except AppError as exc:
            self._record_history(
                operation="copy",
                status="failed",
                source=source,
                destination=destination,
                error_code=exc.code,
                error_message=exc.message,
                finished_at=self._now_iso(),
            )
            raise

    def create_batch_copy_task(self, sources: list[str] | None, destination_base: str | None) -> TaskCreateResponse:
        if not sources or len(sources) < 2:
            raise AppError(
                code="invalid_request",
                message="Batch copy requires at least 2 sources",
                status_code=400,
            )
        if not destination_base:
            raise AppError(
                code="invalid_request",
                message="Destination base is required",
                status_code=400,
            )

        resolved_destination_base = self._path_guard.resolve_directory_path(destination_base)
        items: list[dict] = []
        for source in sources:
            destination = self._join_destination_base(destination_base, self._path_guard.resolve_existing_path(source).absolute.name)
            item = self._build_copy_item(
                source=source,
                destination=destination,
                resolved_destination=resolved_destination_base,
                destination_base=destination_base,
                include_root_prefix=True,
            )
            items.append(item)

        task_id = str(uuid.uuid4())
        task = self._repository.create_task(
            operation="copy",
            source=f"{len(items)} items",
            destination=resolved_destination_base.relative,
            task_id=task_id,
        )
        self._record_history(
            entry_id=task_id,
            operation="copy",
            status="queued",
            source=f"{len(items)} items",
            destination=resolved_destination_base.relative,
        )
        self._runner.enqueue_copy_batch(
            task_id=task["id"],
            items=[
                {
                    "source": item["source_absolute"],
                    "destination": item["destination_absolute"],
                    "kind": item["kind"],
                    "files": item["files"],
                    "directories": item["directories"],
                }
                for item in items
            ],
        )
        return TaskCreateResponse(task_id=task["id"], status=task["status"])

    def _build_copy_item(
        self,
        source: str,
        destination: str,
        resolved_destination: ResolvedPath | None = None,
        destination_base: str | None = None,
        include_root_prefix: bool = False,
    ) -> dict:
        resolved_source = self._path_guard.resolve_existing_path(source)
        _, _, lexical_source, _ = self._path_guard.resolve_lexical_path(source)
        if lexical_source.is_symlink():
            raise AppError(
                code="type_conflict",
                message="Source must not be a symlink",
                status_code=409,
                details={"path": source},
            )

        source_is_file = resolved_source.absolute.is_file()
        source_is_directory = resolved_source.absolute.is_dir()
        if not source_is_file and not source_is_directory:
            raise AppError(
                code="type_conflict",
                message="Unsupported source path type",
                status_code=409,
                details={"path": source},
            )

        resolved_destination = resolved_destination or self._path_guard.resolve_path(destination)
        destination_absolute = (
            resolved_destination.absolute / resolved_source.absolute.name
            if destination_base is not None
            else resolved_destination.absolute
        )
        destination_relative = self._path_guard.entry_relative_path(
            resolved_destination.alias,
            destination_absolute,
            display_style=resolved_destination.display_style,
        )
        destination_parent = destination_absolute.parent
        parent_relative = self._path_guard.entry_relative_path(
            resolved_destination.alias,
            destination_parent,
            display_style=resolved_destination.display_style,
        )
        self._map_directory_validation(parent_relative)

        if destination_absolute.exists():
            raise AppError(
                code="already_exists",
                message="Target path already exists",
                status_code=409,
                details={"path": destination_relative},
            )

        if source_is_directory and self._is_nested_destination(resolved_source.absolute, destination_absolute):
            raise AppError(
                code="invalid_request",
                message="Destination cannot be inside source",
                status_code=400,
                details={"path": source, "destination": destination_relative},
            )

        if source_is_directory:
            directories, files = self._build_directory_plan(
                resolved_source=resolved_source,
                destination_root=destination_absolute,
                include_root_prefix=include_root_prefix,
            )
        else:
            files = [
                {
                    "source": str(resolved_source.absolute),
                    "destination": str(destination_absolute),
                    "label": resolved_source.absolute.name,
                }
            ]
            directories = []

        return {
            "source_relative": resolved_source.relative,
            "destination_relative": destination_relative,
            "source_absolute": str(resolved_source.absolute),
            "destination_absolute": str(destination_absolute),
            "kind": "directory" if source_is_directory else "file",
            "total_bytes": int(resolved_source.absolute.stat().st_size) if source_is_file else None,
            "files": files,
            "directories": directories,
        }

    def _map_directory_validation(self, relative_path: str) -> None:
        try:
            self._path_guard.resolve_directory_path(relative_path)
        except AppError as exc:
            if exc.code == "path_type_conflict":
                raise AppError(
                    code="type_conflict",
                    message="Destination parent is not a directory",
                    status_code=409,
                    details=exc.details,
                )
            raise

    def _build_directory_plan(
        self,
        *,
        resolved_source: ResolvedPath,
        destination_root: Path,
        include_root_prefix: bool,
    ) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
        directories: list[dict[str, str]] = [
            {
                "source": str(resolved_source.absolute),
                "destination": str(destination_root),
            }
        ]
        files: list[dict[str, str]] = []
        for root, dirnames, filenames in os.walk(resolved_source.absolute, followlinks=False):
            root_path = Path(root)
            dirnames.sort(key=str.lower)
            filenames.sort(key=str.lower)
            for name in dirnames:
                entry = root_path / name
                if entry.is_symlink():
                    raise AppError(
                        code="type_conflict",
                        message="Source directory must not contain symlinks",
                        status_code=409,
                        details={"path": resolved_source.relative},
                    )
                relative = entry.relative_to(resolved_source.absolute)
                directories.append(
                    {
                        "source": str(entry),
                        "destination": str(destination_root / relative),
                    }
                )
            for name in filenames:
                entry = root_path / name
                if entry.is_symlink():
                    raise AppError(
                        code="type_conflict",
                        message="Source directory must not contain symlinks",
                        status_code=409,
                        details={"path": resolved_source.relative},
                    )
                relative = entry.relative_to(resolved_source.absolute)
                files.append(
                    {
                        "source": str(entry),
                        "destination": str(destination_root / relative),
                        "label": self._progress_label(
                            top_level_name=resolved_source.absolute.name,
                            relative_path=relative,
                            include_root_prefix=include_root_prefix,
                        ),
                    }
                )
        return directories, files

    @staticmethod
    def _progress_label(*, top_level_name: str, relative_path: Path, include_root_prefix: bool) -> str:
        relative_value = relative_path.as_posix()
        if not relative_value:
            return top_level_name
        return f"{top_level_name}/{relative_value}" if include_root_prefix else relative_value

    @staticmethod
    def _join_destination_base(destination_base: str, name: str) -> str:
        return f"{destination_base.rstrip('/')}/{name}" if destination_base.rstrip("/") else f"/{name}"

    @staticmethod
    def _is_nested_destination(source: Path, destination: Path) -> bool:
        try:
            destination.relative_to(source)
            return True
        except ValueError:
            return False

    def _record_history(self, **kwargs) -> None:
        if self._history_repository:
            self._history_repository.create_entry(**kwargs)

    @staticmethod
    def _now_iso() -> str:
        from datetime import datetime, timezone

        return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
