"""Shared error-formatting helpers.

These keep error messages uniform across modules: same "what failed,
where, and what to try next" structure regardless of which layer
raises. Public CLIs / GUIs can rely on the message format being
consistent enough to surface to end users without further wrapping.

Usage patterns:

    raise DataToolsError(
        "Could not read input file",
        path=path,
        suggestion="Check that the file exists and is readable.",
    )

    # Wrapping a library error:
    try:
        wb = load_workbook(path)
    except (BadZipFile, InvalidFileException) as e:
        raise FileFormatError(
            "Excel file is corrupted or not a valid .xlsx",
            path=path,
            cause=e,
        ) from e
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Iterable, Optional


class DataToolsError(Exception):
    """Base class for all DataTools-raised errors.

    Carries optional structured fields so GUIs / logs can render them
    consistently rather than re-parsing free-form messages.
    """

    def __init__(
        self,
        message: str,
        *,
        path: Optional[Path | str] = None,
        column: Optional[str] = None,
        operation: Optional[str] = None,
        suggestion: Optional[str] = None,
        cause: Optional[BaseException] = None,
    ):
        self.message = message
        self.path = Path(path) if path is not None else None
        self.column = column
        self.operation = operation
        self.suggestion = suggestion
        self.cause = cause
        super().__init__(self.format())

    def format(self) -> str:
        """Render a human-friendly multi-line message."""
        lines = [self.message]
        if self.operation:
            lines.append(f"  while: {self.operation}")
        if self.path:
            lines.append(f"  file: {self.path}")
        if self.column:
            lines.append(f"  column: {self.column!r}")
        if self.cause:
            lines.append(f"  underlying: {type(self.cause).__name__}: {self.cause}")
        if self.suggestion:
            lines.append(f"  suggestion: {self.suggestion}")
        return "\n".join(lines)


class InputValidationError(DataToolsError, ValueError):
    """Caller passed a bad argument — e.g., non-DataFrame, bad enum value."""


class ConfigError(DataToolsError, ValueError):
    """Configuration file or options object is invalid."""


class FileFormatError(DataToolsError, ValueError):
    """File exists but is not in the expected format (corrupted, wrong schema)."""


class FileAccessError(DataToolsError, OSError):
    """File could not be read or written — permissions, missing parent, full disk."""


# ---------------------------------------------------------------------------
# Convenience constructors
# ---------------------------------------------------------------------------


def ensure_dataframe(value: Any, *, function: str, parameter: str = "df") -> None:
    """Raise InputValidationError if *value* isn't a pandas DataFrame.

    Centralizes the repetitive guard so every public entry point gives
    the same message shape.
    """
    import pandas as pd  # lazy — keeps this module dependency-light
    if not isinstance(value, pd.DataFrame):
        raise InputValidationError(
            f"{function}() requires a pandas DataFrame for {parameter!r}",
            operation=function,
            suggestion=(
                f"Got {type(value).__name__}. "
                "Pass a DataFrame loaded via src.core.io.read_file() "
                "or constructed with pd.DataFrame(...)."
            ),
        )


def ensure_choice(
    value: Any,
    *,
    name: str,
    choices: Iterable[Any],
    function: Optional[str] = None,
) -> None:
    """Raise InputValidationError if *value* isn't in *choices*."""
    choices = list(choices)
    if value in choices:
        return
    raise InputValidationError(
        f"Invalid {name}={value!r}",
        operation=function,
        suggestion=f"Valid: {sorted(map(str, choices))}",
    )


def wrap_file_read(path: Path | str, operation: str, exc: BaseException) -> FileAccessError:
    """Build a FileAccessError describing a read failure with helpful context."""
    return FileAccessError(
        f"Could not read file ({type(exc).__name__})",
        path=path,
        operation=operation,
        cause=exc,
        suggestion=(
            "Check that the file exists, you have read permission, and the "
            "path isn't on a network mount that may have disconnected."
        ),
    )


def wrap_file_write(path: Path | str, operation: str, exc: BaseException) -> FileAccessError:
    """Build a FileAccessError describing a write failure with helpful context."""
    suggestion = (
        "Check that the parent directory exists, you have write permission, "
        "and there is enough free disk space."
    )
    if isinstance(exc, PermissionError):
        suggestion = (
            "Check write permissions on the parent directory. "
            "On Windows, also ensure the file is not open in another program."
        )
    return FileAccessError(
        f"Could not write file ({type(exc).__name__})",
        path=path,
        operation=operation,
        cause=exc,
        suggestion=suggestion,
    )


# ---------------------------------------------------------------------------
# Friendly formatter for end-user surfaces (CLI stderr, GUI st.error)
# ---------------------------------------------------------------------------


def format_for_user(exc: BaseException, *, context: Optional[str] = None) -> str:
    """Render an exception for end-user display.

    Recognizes :class:`DataToolsError` and uses its structured fields;
    falls back to a generic message + class name for unrecognized
    exceptions. ``context`` is an optional one-line prefix describing
    what the user was trying to do (e.g., ``"Failed to read upload"``).
    """
    if isinstance(exc, DataToolsError):
        body = exc.format()
    else:
        body = f"{type(exc).__name__}: {exc}"
    if context:
        return f"{context}\n\n{body}"
    return body
