"""GnommoCache - External storage extension for large media files.

Provides transparent fallback to external storage when files are not found locally.
Configure via ~/.gnommo.conf:

    [cache]
    path = /Volumes/GnommoDisk/gnommo

Files are looked up first locally, then in the cache at:
    {cache_path}/{project_name}/{relative_path}
"""

import configparser
import json
import os
from pathlib import Path
from typing import Optional, Tuple

_cache_config: Optional[dict] = None
_assets_config: Optional[dict] = None
_perf_config: Optional[dict] = None
# Per-project performance overrides (project.json "performance" block). Set at the
# start of preprocess/render via set_active_project(). These OVERRIDE ~/.gnommo.conf
# and — unlike that per-machine file — travel with the project over up/down, so the
# render rig's chunk size / CPU limits can be tuned remotely by editing project.json.
_active_project_perf: dict = {}


def _load_perf_config() -> dict:
    """Read and cache the [performance] section of ~/.gnommo.conf."""
    global _perf_config
    if _perf_config is not None:
        return _perf_config

    _perf_config = {}
    config_path = Path.home() / ".gnommo.conf"
    if config_path.exists():
        cfg = configparser.ConfigParser()
        cfg.read(config_path)
        for key in ("cpu_limit_preprocess", "cpu_limit_render", "cpu_limit"):
            if cfg.has_option("performance", key):
                try:
                    _perf_config[key] = float(cfg.get("performance", key))
                except ValueError:
                    pass
        if cfg.has_option("performance", "render_chunk_slides"):
            try:
                _perf_config["render_chunk_slides"] = int(
                    cfg.get("performance", "render_chunk_slides")
                )
            except ValueError:
                pass
    return _perf_config


def set_active_project(project_path) -> None:
    """Load a project's optional "performance" overrides from its project.json.

    project.json syncs via up/down and isn't secret, so its "performance" block is
    the remotely-editable home for the render rig's tunables (render_chunk_slides,
    cpu_limit_preprocess, cpu_limit_render). Keys present here override
    ~/.gnommo.conf; anything absent falls back to the machine config. Call once at
    the start of a per-project preprocess/render.
    """
    global _active_project_perf
    _active_project_perf = {}
    if project_path is None:
        return
    pj = Path(project_path) / "project.json"
    if not pj.exists():
        return
    try:
        perf = json.loads(pj.read_text(encoding="utf-8")).get("performance")
    except (ValueError, OSError):
        return
    if isinstance(perf, dict):
        _active_project_perf = perf


def _perf_get(key: str, default_key: Optional[str] = None):
    """Resolve a performance value: active project.json overrides ~/.gnommo.conf.

    Within each source the stage-specific `key` wins over the legacy `default_key`
    (e.g. `cpu_limit`); the project source is consulted before the machine config.
    """
    conf = _load_perf_config()
    for src in (_active_project_perf, conf):
        for k in (key, default_key):
            if k and src.get(k) is not None:
                return src[k]
    return None


def get_ffmpeg_thread_count(stage: str = "preprocess") -> int:
    """Return the FFmpeg thread count for a pipeline stage from ~/.gnommo.conf.

    Preprocessing and rendering scale differently, so they read separate CPU
    fractions of the logical core count:

        [performance]
        cpu_limit_preprocess = 0.8   # throughput-bound; safe at high parallelism
        cpu_limit_render     = 0.15  # -filter_complex spawns swscaler threads per
                                     # layer and OOMs at high core counts

    `stage` is "preprocess" or "render". The legacy single `cpu_limit` key is the
    fallback for either stage when its specific key is absent. A project.json
    "performance" block (see set_active_project) overrides these per project. Each
    value is a fraction of logical CPUs (0.8 = 80%); defaults to 1 thread when
    nothing is configured, which is safe on memory-constrained machines.
    """
    key = "cpu_limit_render" if stage == "render" else "cpu_limit_preprocess"
    cpu_limit = _perf_get(key, "cpu_limit")
    if cpu_limit is None:
        return 1
    try:
        cpu_limit = float(cpu_limit)
    except (TypeError, ValueError):
        return 1
    cpu_count = os.cpu_count() or 1
    return max(1, int(cpu_count * cpu_limit))


def get_render_chunk_size() -> Optional[int]:
    """Return slides-per-chunk for auto-chunked rendering, or None if not configured.

    When set, cmd_render splits the filter graph into chunks of this many slides
    to avoid OOM from allocating filter buffers for the entire video at once.
    A project.json "performance" block overrides ~/.gnommo.conf per project.

    Example ~/.gnommo.conf:
        [performance]
        render_chunk_slides = 15
    """
    val = _perf_get("render_chunk_slides")
    if val is None:
        return None
    try:
        return max(1, int(val))
    except (ValueError, TypeError):
        return None


def load_cache_config() -> Optional[Path]:
    """Load gnommo.conf and return cache path if configured.

    Configuration file location: ~/.gnommo.conf

    Returns:
        Path to the cache root directory, or None if not configured.
    """
    global _cache_config
    if _cache_config is not None:
        return _cache_config.get("path")

    config_path = Path.home() / ".gnommo.conf"
    if not config_path.exists():
        _cache_config = {}
        return None

    config = configparser.ConfigParser()
    config.read(config_path)

    if config.has_option("cache", "path"):
        cache_path = Path(config.get("cache", "path"))
        _cache_config = {"path": cache_path}
        return cache_path

    _cache_config = {}
    return None


def load_assets_process_cache() -> Optional[Path]:
    """Return the process-cache path on the [assets] disk, or None if not configured.

    Derived by replacing the last component of the [assets] path with
    that name + "cache".  E.g.:
        [assets] path = /Volumes/LaCie Jens/Projects/gnommo
        → process cache  = /Volumes/LaCie Jens/Projects/gnommocache

    This mirrors the GnommoDisk convention where the asset root is
    /Volumes/GnommoDisk/gnommo and the process cache is /Volumes/GnommoDisk/gnommocache.
    """
    assets_path = load_assets_config()
    if assets_path is None:
        return None
    return assets_path.parent / (assets_path.name + "cache")


def load_assets_config() -> Optional[Path]:
    """Load gnommo.conf and return the [assets] path if configured.

    The assets path is a second external fallback (e.g. a LaCie drive) with
    the same directory layout as the gnommo project root.  Resolution order is:
    local → cache ([cache] path) → assets ([assets] path).

    Example ~/.gnommo.conf:
        [assets]
        path = /Volumes/LaCie Jens/Projects/gnommo
    """
    global _assets_config
    if _assets_config is not None:
        return _assets_config.get("path")

    config_path = Path.home() / ".gnommo.conf"
    if not config_path.exists():
        _assets_config = {}
        return None

    config = configparser.ConfigParser()
    config.read(config_path)

    if config.has_option("assets", "path"):
        assets_path = Path(config.get("assets", "path"))
        _assets_config = {"path": assets_path}
        return assets_path

    _assets_config = {}
    return None


def _resolve_against_base(
    local_path: Path, project_path: Path, base: Path
) -> Optional[Path]:
    """Try to find local_path mirrored under base.

    Tries two mappings:
      1. project-relative:  base / project_name / relative_to_project
      2. gnommo-root-relative:  base / relative_to_project_parent  (e.g. shared_assets/…)
    """
    try:
        relative = local_path.relative_to(project_path)
        p = base / project_path.name / relative
        if p.exists():
            return p
    except ValueError:
        pass

    try:
        relative = local_path.relative_to(project_path.parent)
        p = base / relative
        if p.exists():
            return p
    except ValueError:
        pass

    return None


def resolve_with_cache(
    local_path: Path,
    project_path: Path,
) -> Tuple[Path, bool]:
    """Resolve a file path with external-disk fallback (read-only).

    Resolution order:
      1. local_path (always checked first)
      2. [cache] path  — typically GnommoDisk
      3. [assets] path — optional second drive (e.g. LaCie)

    Returns:
        Tuple of (resolved_path, is_from_external) where is_from_external=True
        when the file was found on an external drive rather than locally.
    """
    if local_path.exists():
        return local_path, False

    for base in (load_cache_config(), load_assets_config()):
        if base is None:
            continue
        resolved = _resolve_against_base(local_path, project_path, base)
        if resolved is not None:
            return resolved, True

    return local_path, False


def load_server_config() -> Optional[dict]:
    """Load server rsync config from ~/.gnommo.conf.

    Expected config:
        [server]
        host = 76.13.144.52
        user = root
        path = /gnommo/project

    Returns:
        Dict with keys host, user, path (and optionally port), or None.
    """
    config_path = Path.home() / ".gnommo.conf"
    if not config_path.exists():
        return None

    config = configparser.ConfigParser()
    config.read(config_path)

    if not config.has_section("server"):
        return None

    host = config.get("server", "host", fallback=None)
    user = config.get("server", "user", fallback="root")
    path = config.get("server", "path", fallback="/gnommo/project")
    port = config.get("server", "port", fallback="22")

    if not host:
        return None

    return {"host": host, "user": user, "path": path, "port": port}


def is_cache_configured() -> bool:
    """Check if any external fallback is configured."""
    return load_cache_config() is not None or load_assets_config() is not None


def get_cache_info() -> Optional[str]:
    """Get a human-readable string of all configured external paths."""
    parts = []
    for label, path in (("cache", load_cache_config()), ("assets", load_assets_config())):
        if path is None:
            continue
        status = "connected" if path.exists() else "not connected"
        parts.append(f"{path} ({status})")
    return "; ".join(parts) if parts else None
