"""Override resolution for ENTR Adapter deployment files.

Determines whether to use package-default deployment files or
adapter-provided overrides from the ``deploy/`` directory.

Two resolution mechanisms exist, selected by file type:

- **File-system shadow (full replace):** For ``Dockerfile``, ``nginx.conf``,
  ``fluent-bit.conf``. If the adapter has the file in ``deploy/``, it
  completely replaces the package default.

- **Compose-native merge:** For docker-compose files. The package default is
  always the base. If the adapter has ``docker-compose.override.yml``, Docker
  Compose's built-in merge handles combining them via ``-f base -f override``.

The ``deploy/`` override directory path is part of the public API.
Changing it would be a breaking change requiring a major version bump
(NFR5, NFR6).
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from importlib.resources import files
from pathlib import Path
from typing import Literal

logger = logging.getLogger("entr_adapter_core.defaults.resolve")

# ---------------------------------------------------------------------------
# Public API constant
# ---------------------------------------------------------------------------

DEFAULT_OVERRIDE_DIR: str = "deploy/"
"""Default path to the adapter's override directory, relative to the
adapter project root (current working directory).

.. warning::
    This path is part of the public API. Changing it requires a major
    version bump per NFR5/NFR6.
"""

# ---------------------------------------------------------------------------
# Known deployment files and their resolution mechanisms
# ---------------------------------------------------------------------------

#: Files resolved via file-system shadow (full replace)
SHADOW_FILES: tuple[str, ...] = ("Dockerfile", "nginx.conf", "fluent-bit.conf", "prefix.lua")

#: Files resolved via compose-native merge
COMPOSE_FILES: tuple[str, ...] = (
    "docker-compose.dev.yml",
    "docker-compose.prod.yml",
)

#: Compose override filename (adapter-provided)
COMPOSE_OVERRIDE_FILENAME: str = "docker-compose.override.yml"


# ---------------------------------------------------------------------------
# Result dataclass
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class ResolvedFile:
    """Result of resolving a single deployment file.

    Attributes:
        filename: The deployment file name (e.g. ``Dockerfile``).
        resolved_path: Absolute path to the file that should be used.
        source: Whether the resolved file comes from the installed package
            or an adapter-provided override.
        mechanism: How the override is applied -- ``shadow`` means full
            file replacement, ``compose_merge`` means Docker Compose
            ``-f base -f override`` chaining.
    """

    filename: str
    resolved_path: Path
    source: Literal["package_default", "adapter_override"]
    mechanism: Literal["shadow", "compose_merge"]


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _package_defaults_dir() -> Path:
    """Return the absolute path to the package's ``defaults/`` directory."""
    return Path(str(files("entr_adapter_core") / "defaults"))


def _get_package_default(filename: str) -> Path:
    """Return the path to a package default file, raising if missing."""
    path = _package_defaults_dir() / filename
    if not path.is_file():
        raise FileNotFoundError(
            f"Package default file '{filename}' not found at {path}. "
            "The entr-adapter-core package may be corrupted -- "
            "try reinstalling with: uv add entr-adapter-core --reinstall"
        )
    return path


# ---------------------------------------------------------------------------
# Public resolution functions
# ---------------------------------------------------------------------------

def resolve_file(
    filename: str,
    override_dir: str = DEFAULT_OVERRIDE_DIR,
) -> Path:
    """Resolve a shadow-type deployment file.

    Checks the adapter's override directory first; falls back to the
    package default if no override is found.

    This function is intended for files that use the **file-system shadow**
    mechanism: ``Dockerfile``, ``nginx.conf``, ``fluent-bit.conf``.
    For docker-compose files, use :func:`resolve_compose_files` instead.

    Args:
        filename: Name of the deployment file (e.g. ``"Dockerfile"``).
        override_dir: Path to the adapter's override directory, relative
            to the current working directory. Defaults to ``"deploy/"``.

    Returns:
        Absolute path to the file that should be used.

    Raises:
        FileNotFoundError: If the package default is missing (indicates
            a corrupted installation).
    """
    override_path = Path(override_dir) / filename
    if override_path.is_file():
        return override_path.resolve()

    return _get_package_default(filename)


def resolve_compose_files(
    mode: Literal["dev", "prod"] = "dev",
    override_dir: str = DEFAULT_OVERRIDE_DIR,
) -> list[Path]:
    """Resolve docker-compose files for the specified mode.

    Returns the package default compose file for the given mode as the
    base. If the adapter has a ``docker-compose.override.yml`` in the
    override directory, it is appended for ``-f base -f override``
    chaining.

    Compose files use map-style environment variables to ensure proper
    merge behavior when override files are applied.

    Args:
        mode: Which compose configuration to resolve — ``"dev"`` for
            local development or ``"prod"`` for production.
        override_dir: Path to the adapter's override directory, relative
            to the current working directory. Defaults to ``"deploy/"``.

    Returns:
        Ordered list of absolute paths suitable for passing to
        ``docker compose -f <path1> -f <path2> ...``.

    Raises:
        FileNotFoundError: If the package default compose file is missing.
    """
    filename = f"docker-compose.{mode}.yml"
    result: list[Path] = [_get_package_default(filename)]

    override_path = Path(override_dir) / COMPOSE_OVERRIDE_FILENAME
    if override_path.is_file():
        result.append(override_path.resolve())

    return result


def resolve_all_files(
    override_dir: str = DEFAULT_OVERRIDE_DIR,
) -> dict[str, ResolvedFile]:
    """Resolve all known deployment files at once.

    Combines shadow resolution and compose resolution into a single
    unified result dictionary.

    Args:
        override_dir: Path to the adapter's override directory, relative
            to the current working directory. Defaults to ``"deploy/"``.

    Returns:
        Dictionary keyed by filename, with :class:`ResolvedFile` values
        describing the resolution result for each file.
    """
    resolved: dict[str, ResolvedFile] = {}

    # Shadow-type files
    for filename in SHADOW_FILES:
        path = resolve_file(filename, override_dir)
        is_override = not str(path).startswith(str(_package_defaults_dir()))
        resolved[filename] = ResolvedFile(
            filename=filename,
            resolved_path=path,
            source="adapter_override" if is_override else "package_default",
            mechanism="shadow",
        )

    # Compose-type files (inventory only — lists all compose files and their sources)
    override_path = Path(override_dir) / COMPOSE_OVERRIDE_FILENAME
    has_compose_override = override_path.is_file()

    for filename in COMPOSE_FILES:
        resolved[filename] = ResolvedFile(
            filename=filename,
            resolved_path=_get_package_default(filename),
            source="package_default",
            mechanism="compose_merge",
        )

    if has_compose_override:
        resolved[COMPOSE_OVERRIDE_FILENAME] = ResolvedFile(
            filename=COMPOSE_OVERRIDE_FILENAME,
            resolved_path=override_path.resolve(),
            source="adapter_override",
            mechanism="compose_merge",
        )

    return resolved


def log_resolution_summary(
    resolved_files: dict[str, ResolvedFile],
) -> None:
    """Log a summary table of deployment file resolution at INFO level.

    Ensures every file is explicitly listed with its source and mechanism,
    so there are no silent fallbacks.

    Args:
        resolved_files: Dictionary from :func:`resolve_all_files`.
    """
    # Build table rows
    rows: list[tuple[str, str, str]] = []
    for rf in resolved_files.values():
        source_label = (
            "package default" if rf.source == "package_default"
            else "adapter override"
        )
        mechanism_label = (
            "shadow" if rf.mechanism == "shadow"
            else "compose merge"
        )
        rows.append((rf.filename, source_label, mechanism_label))

    # Calculate column widths
    headers = ("File", "Source", "Mechanism")
    col_widths = [
        max(len(headers[i]), *(len(row[i]) for row in rows))
        for i in range(3)
    ]

    # Format table
    def _row_line(cells: tuple[str, ...], fill: str = " ") -> str:
        parts = [cell.ljust(col_widths[i]) for i, cell in enumerate(cells)]
        return f"│ {' │ '.join(parts)} │"

    separator = (
        "├─"
        + "─┼─".join("─" * w for w in col_widths)
        + "─┤"
    )
    top_border = (
        "┌─"
        + "─┬─".join("─" * w for w in col_widths)
        + "─┐"
    )
    bottom_border = (
        "└─"
        + "─┴─".join("─" * w for w in col_widths)
        + "─┘"
    )

    lines = [
        "Deployment file resolution:",
        top_border,
        _row_line(headers),
        separator,
    ]
    for row in rows:
        lines.append(_row_line(row))
    lines.append(bottom_border)

    logger.info("\n".join(lines))
