from __future__ import annotations

import os
import shutil
from pathlib import Path
from typing import Any, Iterable

from liverylab_core import project_types


def _path_is_within(path: Path, parent: Path) -> bool:
    try:
        return os.path.commonpath([str(path.resolve()), str(parent.resolve())]) == str(parent.resolve())
    except (OSError, ValueError):
        return False


def _resolved_path(value: Any) -> Path | None:
    text = str(value or "").strip()
    if not text:
        return None
    try:
        return Path(text).expanduser().resolve()
    except (OSError, ValueError):
        return None


def eligibility(
    entry: dict[str, Any] | None,
    *,
    storage_root: str | Path,
    local_skins_root: str | Path,
    projects_root: str | Path,
) -> dict[str, Any]:
    data = entry if isinstance(entry, dict) else {}
    reasons: list[str] = []
    marker = data.get("legacy_workspace_migration")
    if not isinstance(marker, dict) or not str(marker.get("migrationId") or "").strip():
        reasons.append("legacy workspace migration marker missing")
    if data.get("active") is not False:
        reasons.append("migrated livery is not explicitly inactive")
    if project_types.project_type_for_entry(data) != project_types.PROJECT_TYPE_ADVANCED_EXTRACTED_CONTENT:
        reasons.append("livery is not an Advanced extracted-content copy")

    storage = _resolved_path(storage_root)
    local_storage = _resolved_path(local_skins_root)
    project_storage = _resolved_path(projects_root)
    if storage is None or local_storage is None or not _path_is_within(local_storage, storage):
        reasons.append("local skin storage root is outside LiveryLab storage")
    if storage is None or project_storage is None or not _path_is_within(project_storage, storage):
        reasons.append("project storage root is outside LiveryLab storage")

    local_root = _resolved_path(data.get("local_root"))
    if local_root is None:
        reasons.append("migrated local skin folder is missing")
    elif local_storage is None or not _path_is_within(local_root, local_storage):
        reasons.append("migrated local skin folder is outside LocalSkins")
    elif not local_root.is_dir():
        reasons.append("migrated local skin folder does not exist")

    project_values = [
        value
        for value in (data.get("project_root"), data.get("project_folder"))
        if str(value or "").strip()
    ]
    project_roots: list[Path] = []
    for value in project_values:
        project_root = _resolved_path(value)
        if project_root is None:
            reasons.append("migrated project folder path is invalid")
            continue
        if project_storage is None or not _path_is_within(project_root, project_storage):
            reasons.append("migrated project folder is outside LiveryProjects")
            continue
        if project_root not in project_roots:
            project_roots.append(project_root)

    return {
        "eligible": not reasons,
        "reasons": reasons,
        "localRoot": str(local_root) if local_root else "",
        "projectRoots": [str(path) for path in project_roots],
        "migrationId": str(marker.get("migrationId") or "") if isinstance(marker, dict) else "",
    }


def move_transaction(moves: Iterable[tuple[str | Path, str | Path]]) -> list[dict[str, str]]:
    planned: list[tuple[Path, Path]] = []
    for source_value, target_value in moves:
        source = Path(source_value).resolve()
        target = Path(target_value).resolve()
        if source == target:
            raise ValueError(f"Migration trash source and target are identical: {source}")
        if not source.exists():
            raise FileNotFoundError(f"Migration trash source not found: {source}")
        if target.exists():
            raise FileExistsError(f"Migration trash target already exists: {target}")
        planned.append((source, target))

    completed: list[tuple[Path, Path]] = []
    try:
        for source, target in planned:
            target.parent.mkdir(parents=True, exist_ok=True)
            shutil.move(str(source), str(target))
            completed.append((source, target))
    except Exception as move_error:
        rollback_errors: list[str] = []
        for source, target in reversed(completed):
            try:
                source.parent.mkdir(parents=True, exist_ok=True)
                shutil.move(str(target), str(source))
            except Exception as rollback_error:
                rollback_errors.append(f"{target} -> {source}: {rollback_error}")
        if rollback_errors:
            raise RuntimeError(
                "Migrated livery trash failed and rollback was incomplete: "
                + "; ".join(rollback_errors)
            ) from move_error
        raise

    return [
        {"source": str(source), "target": str(target)}
        for source, target in completed
    ]

