from __future__ import annotations

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


RAW_WORKSPACE_RELATIVE = Path("_tmp") / "opr"
WINDOWS_LEGACY_MAX_PATH = 259
WINDOWS_LONG_PATH_REQUIREMENT_LENGTH = 280


class OfficialPreviewPathLengthError(OSError):
    pass


def raw_workspace_root(portable_root: str | Path, *, create: bool = False) -> Path:
    root = Path(portable_root).expanduser() / RAW_WORKSPACE_RELATIVE
    if create:
        root.mkdir(parents=True, exist_ok=True)
    return root


def duplicate_portable_folder(portable_root: str | Path) -> bool:
    root = Path(portable_root).expanduser()
    return bool(root.name and root.parent.name and root.name.lower() == root.parent.name.lower())


def _entry_name(entry: Any) -> str:
    return str(getattr(entry, "name", entry) or "").replace("/", "\\").lstrip("\\")


def inspect_extraction_paths(
    raw_root: str | Path,
    entries: Iterable[Any],
    *,
    portable_root: str | Path | None = None,
    platform_name: str | None = None,
) -> dict[str, Any]:
    root = Path(raw_root).expanduser()
    longest_path = ""
    longest_relative = ""
    longest_length = 0
    entry_count = 0
    for entry in entries:
        if bool(getattr(entry, "is_directory", False)):
            continue
        relative = _entry_name(entry)
        if not relative:
            continue
        parts = [part for part in relative.split("\\") if part]
        if not parts or any(part in {".", ".."} for part in parts):
            continue
        target = str(root.joinpath(*parts).absolute())
        entry_count += 1
        if len(target) > longest_length:
            longest_length = len(target)
            longest_path = target
            longest_relative = relative
    active_platform = os.name if platform_name is None else str(platform_name)
    limit = WINDOWS_LEGACY_MAX_PATH if active_platform == "nt" else None
    ready = limit is None or longest_length <= limit
    return {
        "ready": ready,
        "legacyReady": ready,
        "platform": active_platform,
        "limit": limit,
        "entryCount": entry_count,
        "longestPath": longest_path,
        "longestRelativePath": longest_relative,
        "longestPathLength": longest_length,
        "rawRoot": str(root),
        "portableRoot": str(Path(portable_root).expanduser()) if portable_root is not None else "",
        "duplicatePortableFolder": duplicate_portable_folder(portable_root) if portable_root is not None else False,
    }


def windows_long_paths_registry_status() -> dict[str, Any]:
    if os.name != "nt":
        return {"available": True, "enabled": True, "value": None, "error": "", "source": "non_windows"}
    try:
        import winreg

        with winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE,
            r"SYSTEM\CurrentControlSet\Control\FileSystem",
        ) as key:
            value, _value_type = winreg.QueryValueEx(key, "LongPathsEnabled")
        enabled = int(value) == 1
        return {"available": True, "enabled": enabled, "value": int(value), "error": "", "source": "registry"}
    except Exception as exc:
        return {"available": False, "enabled": False, "value": None, "error": str(exc), "source": "registry"}


def windows_long_paths_health() -> dict[str, Any]:
    status = windows_long_paths_registry_status()
    available = bool(status.get("available"))
    enabled = available and bool(status.get("enabled"))
    if enabled:
        message = "Enabled"
    elif available:
        message = "Not enabled"
    else:
        message = "Could not verify the Windows setting"
    return {
        "key": "longPaths",
        "label": "Windows Long Paths",
        "path": r"HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled",
        "role": "Recommended for deeply nested files in the Official and Advanced workflows.",
        "available": available,
        "enabled": enabled,
        "value": status.get("value"),
        "message": message,
        "detail": str(status.get("error") or ""),
    }


def _long_path_probe_target(probe_root: Path, required_path_length: int) -> Path:
    target_length = max(WINDOWS_LEGACY_MAX_PATH + 16, int(required_path_length or 0))
    parent = probe_root
    index = 0
    while len(str((parent / "probe.tmp").absolute())) < target_length:
        parent = parent / f"segment_{index:02d}_{'x' * 32}"
        index += 1
    return parent / "probe.tmp"


def probe_runtime_long_path_support(
    workspace_root: str | Path,
    *,
    required_path_length: int,
) -> dict[str, Any]:
    registry = windows_long_paths_registry_status()
    if os.name != "nt" or int(required_path_length or 0) <= WINDOWS_LEGACY_MAX_PATH:
        return {
            "available": True,
            "attempted": False,
            "writeReady": True,
            "renameReady": True,
            "deleteReady": True,
            "requiredPathLength": int(required_path_length or 0),
            "probePathLength": 0,
            "probeError": "",
            "probeCleanupError": "",
            "registry": registry,
        }

    root = Path(workspace_root).expanduser()
    probe_root = root / f"_long_path_probe_{uuid.uuid4().hex[:12]}"
    target = _long_path_probe_target(probe_root, required_path_length)
    renamed = target.with_name("probe-renamed.tmp")
    available = False
    write_ready = False
    rename_ready = False
    delete_ready = False
    probe_error = ""
    cleanup_error = ""
    try:
        target.parent.mkdir(parents=True, exist_ok=False)
        target.write_bytes(b"LiveryLab long path probe")
        write_ready = target.exists() and target.is_file()
        if not write_ready:
            probe_error = "The temporary long path probe file could not be verified."
        else:
            os.replace(target, renamed)
            rename_ready = renamed.exists() and renamed.is_file() and not target.exists()
            if not rename_ready:
                probe_error = "The temporary long path probe file could not be renamed."
            else:
                renamed.unlink()
                delete_ready = not renamed.exists()
                if not delete_ready:
                    probe_error = "The temporary long path probe file could not be deleted."
        available = bool(write_ready and rename_ready and delete_ready)
    except Exception as exc:
        probe_error = str(exc)
    finally:
        try:
            if probe_root.exists():
                shutil.rmtree(probe_root)
        except Exception as exc:
            cleanup_error = str(exc)
    return {
        "available": available,
        "attempted": True,
        "writeReady": write_ready,
        "renameReady": rename_ready,
        "deleteReady": delete_ready,
        "requiredPathLength": int(required_path_length or 0),
        "probePathLength": max(len(str(target.absolute())), len(str(renamed.absolute()))),
        "probeError": probe_error,
        "probeCleanupError": cleanup_error,
        "registry": registry,
    }


def windows_long_paths_requirement(
    workspace_root: str | Path,
    *,
    required_path_length: int = WINDOWS_LONG_PATH_REQUIREMENT_LENGTH,
) -> dict[str, Any]:
    health = windows_long_paths_health()
    runtime = probe_runtime_long_path_support(
        workspace_root,
        required_path_length=max(WINDOWS_LEGACY_MAX_PATH + 1, int(required_path_length or 0)),
    )
    ready = bool(runtime.get("available"))
    registry = runtime.get("registry") if isinstance(runtime.get("registry"), dict) else {}
    if ready:
        message = "Ready"
    elif bool(registry.get("available")) and not bool(registry.get("enabled")):
        message = "Not enabled"
    elif bool(registry.get("enabled")):
        message = "Enabled in Windows, but unavailable to the running LiveryLab process"
    else:
        message = "Could not verify or use Windows long paths"
    return {
        **health,
        "required": True,
        "ready": ready,
        "runtimeReady": ready,
        "runtime": runtime,
        "registry": registry,
        "requiredPathLength": int(runtime.get("requiredPathLength") or required_path_length),
        "probePathLength": int(runtime.get("probePathLength") or 0),
        "message": message,
        "role": "Required for both Official and Advanced workflows.",
    }


def apply_runtime_long_path_status(status: dict[str, Any], runtime_status: dict[str, Any]) -> dict[str, Any]:
    result = dict(status)
    legacy_ready = bool(status.get("legacyReady", status.get("ready")))
    runtime_available = bool(runtime_status.get("available"))
    result["legacyReady"] = legacy_ready
    result["longPathSupport"] = dict(runtime_status)
    result["ready"] = bool(legacy_ready or runtime_available)
    result["readyReason"] = "legacy_limit" if legacy_ready else ("runtime_long_paths" if runtime_available else "long_paths_unavailable")
    return result


def path_length_error_message(status: dict[str, Any]) -> str:
    length = int(status.get("longestPathLength") or 0)
    limit = int(status.get("limit") or WINDOWS_LEGACY_MAX_PATH)
    support = status.get("longPathSupport") if isinstance(status.get("longPathSupport"), dict) else {}
    registry = support.get("registry") if isinstance(support.get("registry"), dict) else {}
    registry_label = "enabled" if registry.get("enabled") is True else ("disabled" if registry.get("available") else "unknown")
    probe_label = "passed" if support.get("available") is True else ("failed" if support.get("attempted") else "not run")
    lines = [
        "Windows Long Path Support Required",
        "",
        "Official Preview Resources could not be prepared because Windows long path support is not available to the running LiveryLab process.",
        "No Preview Resources were extracted or changed.",
        "",
        "Recommended fix:",
        "1. Close LiveryLab.",
        "2. Run Enable_Long_Windows_Paths.bat as administrator.",
        "3. Restart Windows.",
        "4. Start LiveryLab and prepare the Preview Resources again.",
        "",
        "Manual alternative:",
        "If you do not want to use the included BAT file, search online for how to enable Win32 long paths in Windows, apply the setting manually, and restart Windows before trying again.",
    ]
    lines.extend(
        [
            "",
            "Details:",
            f"Current longest path: {length} characters",
            f"Windows legacy limit: {limit} characters",
            f"LongPathsEnabled: {registry_label}",
            f"Runtime long path test: {probe_label}",
        ]
    )
    probe_error = str(support.get("probeError") or "").strip()
    if probe_error:
        lines.append(f"Runtime test error: {probe_error}")
    longest = str(status.get("longestPath") or "").strip()
    if longest:
        lines.append(f"Path: {longest}")
    return "\n".join(lines)


def require_safe_extraction_paths(status: dict[str, Any]) -> None:
    if status.get("ready") is True:
        return
    raise OfficialPreviewPathLengthError(path_length_error_message(status))


def cleanup_raw_cache(raw_preview_assets_root: str | Path, *, raw_dirname: str = "Raw") -> dict[str, Any]:
    root = Path(raw_preview_assets_root).expanduser()
    raw = root / raw_dirname
    result = {"removed": False, "path": str(raw), "reason": ""}
    if not raw.exists():
        result["reason"] = "raw cache missing"
        return result
    resolved_root = root.resolve()
    resolved_raw = raw.resolve()
    if resolved_raw.parent != resolved_root or resolved_raw.name != raw_dirname:
        raise RuntimeError(f"Refusing to remove unexpected Raw cache path: {raw}")
    shutil.rmtree(raw)
    result["removed"] = True
    return result
