# api/deps.py
"""FastAPI dependency injection: project resolution, path computation, store/runner access.

v2 layout (post project-paths-refactor 2026-05-26):
_paths_for_project() returns a PathsBundle wrapping ProjectPaths. The dict-style
interface (paths["frames_dir"], paths["refs_dir"], etc.) is preserved as a
one-cycle compat layer — Phase 7 sweeps callers to direct attribute access.
"""

import sys
import threading
from pathlib import Path
from fastapi import Query, HTTPException

PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from recoil.core.paths import (  # noqa: E402 — must follow sys.path setup
    ProjectPaths,
    get_config,
)
from . import state  # noqa: E402
from .state import PROJECT_ROOT as STATE_PROJECT_ROOT  # noqa: E402


class PathsBundle:
    """One-cycle compat wrapper: ProjectPaths + dict-style __getitem__.

    Reads route handlers that index by string ("frames_dir", "refs_dir", etc.)
    continue to work. New code should access attributes on .paths directly.
    """
    __slots__ = ("paths", "project", "_dict_view")

    def __init__(self, paths: ProjectPaths):
        self.paths = paths
        self.project = paths.project
        # Materialize the legacy dict view ONCE, lazily-ish.
        self._dict_view = {
            "project": paths.project,
            "project_dir": paths.project_root,
            # v3 names — the canonical accessors going forward
            "assets_dir": paths.assets_dir,
            "prep_dir": paths.prep_dir,
            "renders_dir": paths.renders_dir,
            "state_dir": paths.state_dir,
            "visual_state_dir": paths.visual_state_dir,
            "manifests_dir": paths.manifests_dir,
            "bundles_dir": paths.bundles_dir,
            "plans_dir": paths.plans_dir,
            "bible_path": paths.global_bible_path,
            "casting_state_path": paths.casting_state_path,
            # Legacy keys mapped to v3 destinations — for unmigrated callers
            "output_dir": paths.project_root / "output",  # raises on stat — see below
            "sequences_dir": paths.prep_dir,  # sequences/ renamed to prep/ in v3
            "frames_dir": paths.prep_dir,  # frames absorbed into prep/
            "previs_dir": paths.prep_dir,
            "video_dir": paths.renders_dir,
            "refs_dir": paths.assets_dir,
            "character_refs_dir": paths.asset_class_dir("char"),
            "location_refs_dir": paths.asset_class_dir("loc"),
        }

    def __getitem__(self, key):
        return self._dict_view[key]

    def __contains__(self, key):
        return key in self._dict_view

    def get(self, key, default=None):
        return self._dict_view.get(key, default)


def _paths_for_project(project: str) -> PathsBundle:
    """Return a PathsBundle for any project. Single Source of Truth (v2)."""
    proj = project.lower()
    return PathsBundle(ProjectPaths.for_project(proj))


def to_serving_path(abs_path: Path, paths) -> str:
    """Convert an absolute path to a frontend-friendly relative path.

    v2: serving roots are 'assets/', 'sequences/', 'renders/', 'state/'.
    Legacy 'output/' prefix is no longer emitted.
    """
    abs_str = str(abs_path)
    project_dir = str(paths.paths.project_root if isinstance(paths, PathsBundle) else paths["project_dir"])
    if abs_str.startswith(project_dir):
        rel = abs_str[len(project_dir):].lstrip("/")
        return rel
    # Universal shared assets (expression library)
    expressions_dir = str(STATE_PROJECT_ROOT / "assets" / "expressions")
    if abs_str.startswith(expressions_dir):
        return "assets/expressions/" + abs_str[len(expressions_dir):].lstrip("/")
    raise ValueError(
        f"Path {abs_str} outside project root ({project_dir}) and not a shared asset"
    )


def get_project_aspect_ratio(project):
    """Read aspect ratio from project_config.json or global config."""
    bundle = _paths_for_project(project)
    config_path = bundle.paths.project_config_path
    if config_path.exists():
        try:
            import json
            cfg = json.loads(config_path.read_text(encoding="utf-8"))
            return str(cfg.get("aspect_ratio", "9:16"))
        except Exception:
            pass
    return str(get_config().get("production_aspect_ratio", "9:16"))


# ── FastAPI dependencies ──────────────────────────────────────────

def get_project(project: str = Query(None)) -> str:
    p = project or state.default_project
    if not p:
        raise HTTPException(503, "No project specified and no default configured")
    return p.lower()


def get_paths(project: str = Query(None)) -> PathsBundle:
    return _paths_for_project(get_project(project))


# ── Store cache ─────────────────────────────────────────────────

_stores: dict = {}
_stores_lock = threading.Lock()


def get_store(project: str = Query(None)):
    p = get_project(project)
    if p in _stores:
        return _stores[p]
    with _stores_lock:
        if p not in _stores:
            try:
                from recoil.execution.execution_store import ExecutionStore
                _stores[p] = ExecutionStore(project=p)
            except Exception as e:
                raise HTTPException(503, f"ExecutionStore not available for {p}: {e}")
        return _stores[p]


def get_runner(project: str, episode: int):
    from recoil.execution.step_runner import StepRunner
    from recoil.execution.step_types import ProjectPaths as StepProjectPaths
    store = get_store(project)
    paths = StepProjectPaths.for_episode(project, episode)
    return StepRunner(store=store, paths=paths, episode=episode)
