"""Local file storage backend — JSON files on disk.

Traces stored as {directory}/traces/{project}/{trace_id}.json
"""

from __future__ import annotations

import json
import logging
import os
import tempfile
from typing import Any, Iterator

from ..models import Trace
from .base import StoreBase, sanitize_project_name

logger = logging.getLogger(__name__)


class LocalStore(StoreBase):
    """Local file-based storage backend."""

    def __init__(self, directory: str) -> None:
        self._directory = directory
        self._traces_dir = os.path.join(directory, "traces")

    def validate(self) -> str | None:
        try:
            os.makedirs(self._traces_dir, exist_ok=True)
            return None
        except OSError as e:
            return f"Cannot create directory: {e}"

    # --- Trace operations ---

    def write_trace(self, trace: Trace) -> None:
        data = json.dumps(trace.to_dict(), indent=2)
        project = sanitize_project_name(trace.project)
        project_dir = os.path.join(self._traces_dir, project)
        _atomic_write(project_dir, f"{trace.trace_id}.json", data)

    def _read_trace(self, trace_id: str, project: str | None = None) -> dict[str, Any] | None:
        basename = os.path.basename(trace_id)
        if basename != trace_id:
            return None

        # Fast path: if project is known, go directly to the file
        if project:
            safe_project = sanitize_project_name(project)
            path = os.path.join(self._traces_dir, safe_project, f"{basename}.json")
            return _read_json(path)

        # Slow path: scan project subdirectories
        if not os.path.isdir(self._traces_dir):
            return None
        for entry in os.listdir(self._traces_dir):
            project_dir = os.path.join(self._traces_dir, entry)
            if os.path.isdir(project_dir):
                path = os.path.join(project_dir, f"{basename}.json")
                trace = _read_json(path)
                if trace is not None:
                    return trace
        return None

    def iter_traces(self, project: str | None = None) -> Iterator[dict[str, Any]]:
        if not os.path.isdir(self._traces_dir):
            return
        if project is not None:
            # Read only from the project subdirectory
            safe_project = sanitize_project_name(project)
            project_dir = os.path.join(self._traces_dir, safe_project)
            if os.path.isdir(project_dir):
                yield from _iter_json_dir(project_dir)
        else:
            # Read from all project subdirectories
            for entry in os.listdir(self._traces_dir):
                project_dir = os.path.join(self._traces_dir, entry)
                if os.path.isdir(project_dir):
                    yield from _iter_json_dir(project_dir)

    def list_projects(self) -> list[str]:
        if not os.path.isdir(self._traces_dir):
            return []
        projects = []
        for entry in os.listdir(self._traces_dir):
            if os.path.isdir(os.path.join(self._traces_dir, entry)):
                projects.append(entry)
        return sorted(projects)


# --- Helpers ---


def _iter_json_dir(directory: str) -> Iterator[dict[str, Any]]:
    """Yield parsed JSON dicts from all .json files in a directory."""
    for fname in os.listdir(directory):
        if fname.endswith(".json"):
            trace = _read_json(os.path.join(directory, fname))
            if trace:
                yield trace


def _atomic_write(directory: str, filename: str, data: str) -> None:
    """Write data to a file atomically via temp file + rename."""
    os.makedirs(directory, exist_ok=True)
    dest = os.path.join(directory, filename)
    fd, tmp = tempfile.mkstemp(dir=directory, suffix=".tmp")
    try:
        os.write(fd, data.encode())
        os.close(fd)
        fd = -1
        os.replace(tmp, dest)
    except BaseException:
        if fd >= 0:
            try:
                os.close(fd)
            except OSError:
                pass
        if os.path.exists(tmp):
            os.unlink(tmp)
        raise


def _read_json(path: str) -> dict[str, Any] | None:
    try:
        with open(path) as f:
            return json.load(f)
    except OSError:
        return None
    except json.JSONDecodeError as e:
        logger.warning(f"Corrupted JSON in {path}: {e}")
        return None
