"""Accept verified filesystem changes into a manifest baseline.

Implements the AIDE ``--update`` pattern: after an integrity check reveals
changes, this module applies those changes to the manifest so the baseline
reflects the current filesystem state.

New/modified entries are added with ``attested=False`` — run
``census scan --resume`` afterward to attest the new hashes.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path

from certisigma import hash_file

from .manifest import Manifest, ManifestEntry
from .evidence import IntegrityReport


@dataclass
class UpdateResult:
    """Outcome of a manifest update operation."""

    removed: int = 0
    updated: int = 0
    added: int = 0
    errors: list[str] = field(default_factory=list)

    @property
    def total_changes(self) -> int:
        return self.removed + self.updated + self.added


def _resolve_within_root(root: Path, rel_path: str) -> Path:
    """Resolve *rel_path* relative to *root* and verify containment.

    Raises ``ValueError`` if the resolved path escapes *root* (e.g. via ``..``
    or absolute paths pointing elsewhere).
    """
    resolved_root = root.resolve()
    if Path(rel_path).is_absolute():
        fpath = Path(rel_path).resolve()
    else:
        fpath = (root / rel_path).resolve()
    if not fpath.is_relative_to(resolved_root):
        raise ValueError(f"Path escapes root: {rel_path}")
    return fpath


def update_manifest_from_integrity(
    manifest: Manifest,
    report: IntegrityReport,
    *,
    show_progress: bool = False,
) -> UpdateResult:
    """Apply integrity-check discrepancies to *manifest*.

    - **missing** files → removed from manifest
    - **modified** files → re-hashed, entry replaced
    - **new** files → hashed and added

    All new/updated entries have ``attested=False``.
    Paths that escape ``report.root_dir`` are rejected as errors.
    """
    from contextlib import nullcontext

    import click

    root = Path(report.root_dir)
    result = UpdateResult()

    total = len(report.missing) + len(report.modified) + len(report.new_files)
    ctx_mgr = (
        click.progressbar(length=total, label="Updating manifest", show_pos=True)
        if show_progress and total
        else nullcontext(None)
    )

    with ctx_mgr as bar:
        for path_str in report.missing:
            if manifest.remove_by_path(path_str):
                result.removed += 1
            if bar:
                bar.update(1)

        for mod in report.modified:
            manifest.remove_by_path(mod.path)
            try:
                fpath = _resolve_within_root(root, mod.path)
                stat = fpath.stat()
                entry = ManifestEntry(
                    hash_hex=mod.actual_hash,
                    path=mod.path,
                    size=stat.st_size,
                    mtime=stat.st_mtime,
                    attested=False,
                )
                manifest.add(entry)
                result.updated += 1
            except Exception as exc:
                result.errors.append(f"{mod.path}: {exc}")
            if bar:
                bar.update(1)

        for new_path in report.new_files:
            try:
                fpath = _resolve_within_root(root, new_path)
                stat = fpath.stat()
                h = hash_file(str(fpath))
                entry = ManifestEntry(
                    hash_hex=h,
                    path=new_path,
                    size=stat.st_size,
                    mtime=stat.st_mtime,
                    attested=False,
                )
                manifest.add(entry)
                result.added += 1
            except Exception as exc:
                result.errors.append(f"{new_path}: {exc}")
            if bar:
                bar.update(1)

    return result
