"""Batch metadata sanity checking and validation.

This module provides targeted hardening to detect missing or corrupted batch
metadata early and produce clear error messages.
"""

from __future__ import annotations

from collections.abc import Mapping

from .query import read_batch_metadata
from .metadata_types import BatchMetadataDict
from .references import get_batch_state_ref_name, get_legacy_batch_ref_name
from ...exceptions import BatchMetadataError
from ...git_paths import display_path
from ...i18n import _
from ...utils.git_command import run_git_command
from ...utils.git_object_io import GitObjectInfo, resolve_git_objects
from ...utils.repository_buffers import git_object_name_is_batch_protocol_safe
from ...utils.paths import get_batch_metadata_file_path


def validate_batch_metadata_file_exists(batch_name: str) -> None:
    """Verify that compatibility metadata exists when only legacy refs exist.

    Args:
        batch_name: Name of the batch

    Raises:
        BatchMetadataError: If metadata file is missing for an existing batch
    """
    state_result = run_git_command(
        ["show-ref", "--verify", "--quiet", get_batch_state_ref_name(batch_name)],
        check=False,
        requires_index_lock=False,
    )
    if state_result.returncode == 0:
        return

    result = run_git_command(
        ["show-ref", "--verify", "--quiet", get_legacy_batch_ref_name(batch_name)],
        check=False,
        requires_index_lock=False,
    )
    batch_ref_exists = result.returncode == 0

    metadata_path = get_batch_metadata_file_path(batch_name)
    metadata_exists = metadata_path.exists()

    # Detect metadata/ref mismatch
    if batch_ref_exists and not metadata_exists:
        raise BatchMetadataError(
            _("Batch metadata is missing or corrupted for '{name}'.\n"
              "The legacy batch ref exists (refs/batches/{name}) but metadata file is missing.\n"
              "Expected metadata file: {file}\n"
              "The batch may not be recoverable automatically.").format(
                name=batch_name,
                file=display_path(str(metadata_path))
            )
        )

    if not batch_ref_exists and metadata_exists:
        # Metadata without a ref is ignored; the batch effectively does not exist.
        pass


def validate_batch_metadata_structure(
    metadata: BatchMetadataDict,
    batch_name: str,
) -> None:
    """Verify that batch metadata has required structure and fields.

    Args:
        metadata: Parsed batch metadata dictionary
        batch_name: Name of the batch (for error messages)

    Raises:
        BatchMetadataError: If metadata structure is invalid or missing required fields
    """
    # Handle empty metadata (can occur after abort or other edge cases)
    # Treat as valid but empty batch rather than corruption
    if not metadata or metadata == {}:
        return

    # Check for required top-level fields
    if "baseline" not in metadata:
        raise BatchMetadataError(
            _("Batch metadata for '{name}' is missing required field: 'baseline'.\n"
              "The metadata file may be corrupted.").format(name=batch_name)
        )

    baseline = metadata.get("baseline")
    if baseline is None:
        raise BatchMetadataError(
            _("Batch '{name}' has no baseline object.\n"
              "The batch metadata exists but baseline is null or missing.\n"
              "This indicates corrupted or incomplete batch state.").format(name=batch_name)
        )

    # Validate files structure if present
    source_commits: list[tuple[str, str, str | None]] = []
    if "files" in metadata:
        files = metadata["files"]
        if not isinstance(files, dict):
            raise BatchMetadataError(
                _("Batch metadata for '{name}' has invalid 'files' field (expected object).\n"
                  "The metadata file may be corrupted.").format(name=batch_name)
            )

        # Validate each file entry has required fields
        for file_path, file_meta in files.items():
            if not isinstance(file_meta, dict):
                raise BatchMetadataError(
                    _("Batch metadata for '{name}' has invalid file entry for '{file}'.\n"
                      "The metadata file may be corrupted.").format(
                        name=batch_name,
                        file=display_path(file_path)
                    )
                )

            # Check for batch_source_commit (required for text files)
            if "batch_source_commit" not in file_meta:
                # Atomic files may not have batch_source_commit because they
                # use whole-entry storage rather than line ownership.
                if file_meta.get("file_type") not in {"binary", "gitlink"}:
                    raise BatchMetadataError(
                        _("Batch metadata for '{name}' is missing 'batch_source_commit' for file '{file}'.\n"
                          "The metadata file may be corrupted.").format(
                            name=batch_name,
                            file=display_path(file_path)
                        )
                    )

            # Validate batch source commit exists
            batch_source_commit = file_meta.get("batch_source_commit")
            if batch_source_commit:
                source_commits.append(
                    (file_path, batch_source_commit, file_meta.get("source_path"))
                )

    object_names = [
        *([baseline] if baseline else []),
        *(commit for _file_path, commit, _source_path in source_commits),
        *(
            f"{get_batch_state_ref_name(batch_name)}:{source_path}"
            for _file_path, _commit, source_path in source_commits
            if source_path
        ),
    ]
    batch_object_names = [
        object_name
        for object_name in object_names
        if isinstance(object_name, str)
        and git_object_name_is_batch_protocol_safe(object_name)
    ]
    object_info_by_name = resolve_git_objects(batch_object_names)
    if baseline and not _git_object_exists(baseline, object_info_by_name):
        raise BatchMetadataError(
            _("Batch '{name}' has invalid baseline object: {baseline}\n"
              "The baseline object does not exist in the repository.\n"
              "The batch metadata may be corrupted.").format(
                name=batch_name,
                baseline=baseline
            )
        )
    for file_path, batch_source_commit, source_path in source_commits:
        embedded_source = (
            f"{get_batch_state_ref_name(batch_name)}:{source_path}"
            if source_path
            else None
        )
        if not _git_object_exists(batch_source_commit, object_info_by_name) and not (
            embedded_source
            and _git_object_exists(embedded_source, object_info_by_name)
        ):
            raise BatchMetadataError(
                _("Batch '{name}' has invalid batch_source_commit for file '{file}': {commit}\n"
                  "The batch source commit does not exist in the repository.\n"
                  "The batch metadata may be corrupted.").format(
                    name=batch_name,
                    file=display_path(file_path),
                    commit=batch_source_commit
                )
            )


def _git_object_exists(
    object_name: str,
    object_info_by_name: Mapping[str, GitObjectInfo],
) -> bool:
    """Check unusual metadata names through argv, outside the batch protocol."""
    if git_object_name_is_batch_protocol_safe(object_name):
        return object_name in object_info_by_name
    result = run_git_command(
        ["cat-file", "-e", object_name],
        check=False,
        requires_index_lock=False,
    )
    return result.returncode == 0


def load_and_validate_batch_metadata(batch_name: str) -> BatchMetadataDict:
    """Load batch metadata and validate its structure.

    This is the primary entry point for commands that need batch metadata.
    It validates the metadata structure and returns clean metadata.

    Args:
        batch_name: Name of the batch

    Returns:
        Validated batch metadata dictionary

    Raises:
        BatchMetadataError: If metadata is missing, corrupted, or structurally invalid
    """
    state_result = run_git_command(
        ["show-ref", "--verify", "--quiet", get_batch_state_ref_name(batch_name)],
        check=False,
        requires_index_lock=False,
    )
    state_ref_exists = state_result.returncode == 0

    legacy_result = run_git_command(
        ["show-ref", "--verify", "--quiet", get_legacy_batch_ref_name(batch_name)],
        check=False,
        requires_index_lock=False,
    )
    batch_ref_exists = state_ref_exists or legacy_result.returncode == 0

    if not batch_ref_exists:
        return {
            "note": "",
            "created_at": "",
            "baseline": None,
            "files": {}
        }

    if batch_ref_exists and not state_ref_exists:
        # Ensure metadata file exists for this batch
        validate_batch_metadata_file_exists(batch_name)

    try:
        metadata = read_batch_metadata(batch_name)
    except Exception as e:
        raise BatchMetadataError(
            _("Failed to read batch metadata for '{name}'.\n"
              "Error: {error}").format(
                name=batch_name,
                error=str(e)
            )
        )

    # Validate structure
    validate_batch_metadata_structure(metadata, batch_name)

    # Normalize and return
    return {
        "revision": metadata.get("revision"),
        "note": metadata.get("note", ""),
        "created_at": metadata.get("created_at", ""),
        "baseline": metadata.get("baseline", None),
        "files": metadata.get("files", {})
    }


def get_validated_baseline_commit(batch_name: str) -> str:
    """Get the commit-or-tree baseline for a batch with validation.

    This is a helper for commands that require a valid baseline commit.
    It validates metadata structure and ensures baseline is present.

    Args:
        batch_name: Name of the batch

    Returns:
        Baseline object ID (guaranteed non-None)

    Raises:
        BatchMetadataError: If metadata is missing, corrupted, or baseline is None
    """
    metadata = load_and_validate_batch_metadata(batch_name)
    baseline = metadata.get("baseline")

    if not baseline:
        raise BatchMetadataError(
            _("Batch '{name}' has no baseline commit.\n"
              "The batch metadata exists but baseline is null or missing.\n"
              "This indicates corrupted or incomplete batch state.").format(name=batch_name)
        )

    return baseline


def read_validated_batch_metadata(batch_name: str) -> BatchMetadataDict:
    """Read and validate batch metadata (command entry point helper).

    This is a drop-in replacement for read_batch_metadata that adds validation.
    Commands should use this to get clear errors early if metadata is corrupted.

    Args:
        batch_name: Name of the batch

    Returns:
        Validated batch metadata dictionary

    Raises:
        BatchMetadataError: If metadata is missing, corrupted, or structurally invalid
    """
    return load_and_validate_batch_metadata(batch_name)
