"""Lockfile management for reproducible skill installs.

The lockfile (agr.lock) pins exact git commit SHAs for every resolved
dependency so that ``agr sync`` produces identical results across
machines and over time.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path

import tomlkit
from tomlkit import TOMLDocument
from tomlkit.exceptions import TOMLKitError

from agr.config import Dependency
from agr.exceptions import ConfigError

LOCKFILE_FILENAME = "agr.lock"
LOCKFILE_VERSION = 1


@dataclass
class LockedSkill:
    """A single locked dependency entry.

    Remote skills have handle/source/commit/content_hash.
    Local skills have path only (resolved from disk at sync time).
    Both have installed_name.
    """

    installed_name: str
    # Remote fields
    handle: str | None = None
    source: str | None = None
    commit: str | None = None
    content_hash: str | None = None
    # Local field
    path: str | None = None

    @property
    def is_local(self) -> bool:
        return self.path is not None

    @property
    def identifier(self) -> str:
        """Unique identifier matching Dependency.identifier."""
        return self.path or self.handle or ""


@dataclass
class Lockfile:
    """The full lockfile state."""

    version: int = LOCKFILE_VERSION
    skills: list[LockedSkill] = field(default_factory=list)


def build_lockfile_path(config_path: Path) -> Path:
    """Return the lockfile path alongside the given config path."""
    return config_path.parent / LOCKFILE_FILENAME


def load_lockfile(path: Path) -> Lockfile | None:
    """Load a lockfile from disk.

    Returns None if the file does not exist.
    Raises ConfigError if the file is malformed.
    """
    if not path.exists():
        return None

    try:
        content = path.read_text()
        doc = tomlkit.parse(content)
    except (TOMLKitError, OSError) as e:
        raise ConfigError(f"Invalid lockfile {path}: {e}") from e

    version = doc.get("version", LOCKFILE_VERSION)
    if version != LOCKFILE_VERSION:
        raise ConfigError(
            f"Unsupported lockfile version {version} (expected {LOCKFILE_VERSION})"
        )

    skills: list[LockedSkill] = []
    for item in doc.get("skill", []):
        if not isinstance(item, dict):
            continue
        skills.append(
            LockedSkill(
                installed_name=str(item.get("installed-name", "")),
                handle=item.get("handle"),
                source=item.get("source"),
                commit=item.get("commit"),
                content_hash=item.get("content-hash"),
                path=item.get("path"),
            )
        )

    return Lockfile(version=version, skills=skills)


def save_lockfile(lockfile: Lockfile, path: Path) -> None:
    """Write a lockfile to disk."""
    doc: TOMLDocument = tomlkit.document()
    doc.add(tomlkit.comment("This file is auto-generated by agr. Do not edit."))
    doc.add(tomlkit.nl())
    doc["version"] = lockfile.version

    skills_aot = tomlkit.aot()
    for skill in lockfile.skills:
        table = tomlkit.table()
        if skill.handle is not None:
            table["handle"] = skill.handle
        if skill.path is not None:
            table["path"] = skill.path
        if skill.source is not None:
            table["source"] = skill.source
        if skill.commit is not None:
            table["commit"] = skill.commit
        if skill.content_hash is not None:
            table["content-hash"] = skill.content_hash
        table["installed-name"] = skill.installed_name
        skills_aot.append(table)

    doc["skill"] = skills_aot
    path.write_text(tomlkit.dumps(doc))


def find_locked_skill(lockfile: Lockfile, dep: Dependency) -> LockedSkill | None:
    """Look up a dependency's entry in the lockfile."""
    identifier = dep.identifier
    for skill in lockfile.skills:
        if skill.identifier == identifier:
            return skill
    return None


def is_lockfile_current(lockfile: Lockfile, dependencies: list[Dependency]) -> bool:
    """Check if the lockfile covers exactly the same deps as agr.toml.

    Returns True only if the lockfile has entries for all dependencies
    and no extra entries. Does not check whether SHAs are stale.
    """
    lockfile_ids = {s.identifier for s in lockfile.skills}
    config_ids = {d.identifier for d in dependencies}
    return lockfile_ids == config_ids


def update_lockfile_entry(lockfile: Lockfile, entry: LockedSkill) -> None:
    """Add or replace an entry in the lockfile by identifier."""
    lockfile.skills = [s for s in lockfile.skills if s.identifier != entry.identifier]
    lockfile.skills.append(entry)


def remove_lockfile_entry(lockfile: Lockfile, identifier: str) -> None:
    """Remove an entry from the lockfile by identifier."""
    lockfile.skills = [s for s in lockfile.skills if s.identifier != identifier]
