"""
Custom exception classes for config error handling.
"""

from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from .context import ConfigErrorContext
    from pydantic_core import ErrorDetails

from ...util.error import Diagnostic, RichEmitter
from ...util.docutils import include_in_docs

# include this module in documentation
__include_in_docs__ = True


@dataclass
@include_in_docs
class AttemptedSource:
    """
    Information about a config source that was tried but failed.

    Attributes
    ----------
    source_name : str
        Name of the source (e.g., "RAIConfig (raiconfig.yaml)")
    error_message : str
        Why it failed (e.g., "File not found", "YAML parsing error")
    file_path : str | None
        Path to the file that was tried
    """
    source_name: str
    error_message: str
    file_path: str | None = None


@include_in_docs
class ConfigError(Exception):
    """Represents a configuration error.

    Config loading and validation objects raise subclasses of this exception.

    Parameters
    ----------
    message : str
        Human-readable error message.
    context : :class:`~relationalai.config.errors.ConfigErrorContext`, optional
        Location information (file, profile, connection) when known.

    Attributes
    ----------
    message : str
        Human-readable error message.
    context : :class:`~relationalai.config.errors.ConfigErrorContext`, optional
        Location information (file, profile, connection) when known.
    """

    def __init__(self, message: str, context: ConfigErrorContext | None = None):
        """
        Initialize config error.

        Args:
            message: Human-readable error message
            context: Optional context about where error occurred
        """
        self.message = message
        self.context = context
        super().__init__(message)

    def __str__(self) -> str:
        """
        Format error message as plain text for tracebacks.

        Returns simple text message. Use format_rich() for pretty output.
        """
        if self.context:
            return f"{self.message}\n\nLocation: {self.context.format_location()}"
        return self.message

    def format_rich(self) -> str:
        """Format error with Rich styling (panels, colors, etc.)."""
        parts = []
        if self.context:
            parts.append(f"Location: {self.context.format_location()}")

        diag = Diagnostic(
            name="ConfigError",
            message=self.message,
            parts=parts,
            severity="error"
        )
        return RichEmitter().emit(diag)


@include_in_docs
class ConfigFileNotFoundError(ConfigError, FileNotFoundError):
    """Raised when no configuration file can be found.

    This error is typically raised by :func:`~relationalai.config.create_config` when it
    cannot discover a config file in any supported location.

    Parameters
    ----------
    message : str
        Human-readable error message.
    attempted_sources : list[AttemptedSource], optional
        Sources that were checked and why each one failed.

    Attributes
    ----------
    attempted_sources : list[AttemptedSource]
        Structured information about each attempted source.
    """

    def __init__(
        self,
        message: str,
        attempted_sources: list[AttemptedSource] | None = None
    ):
        """
        Initialize file not found error.

        Args:
            message: Human-readable error message
            attempted_sources: List of AttemptedSource objects with structured info
        """
        super().__init__(message)
        self.attempted_sources = attempted_sources or []

    def __str__(self) -> str:
        """
        Format as plain text for tracebacks.

        Returns simple text message. Use format_rich() for pretty output.
        """
        if not self.attempted_sources:
            return self.message

        lines = [self.message, ""]
        for source in self.attempted_sources:
            lines.append(f"  ❌ {source.source_name}: {source.error_message}")
            if source.file_path:
                lines.append(f"     File: {source.file_path}")

        return "\n".join(lines)

    def format_rich(self) -> str:
        """Format error with Rich styling (panels, colors, etc.)."""
        parts = []

        # Add attempted sources
        if self.attempted_sources:
            sources_text = "[bold red]Attempted Sources:[/bold red]\n\n"
            for source in self.attempted_sources:
                sources_text += f"  ❌ [bold]{source.source_name}[/bold]\n"
                if source.file_path:
                    sources_text += f"     File: {source.file_path}\n"
                sources_text += f"     Error: [yellow]{source.error_message}[/yellow]\n\n"
            parts.append(sources_text)

        # Add fix suggestions
        fix_text = (
            "[bold green]How to Fix:[/bold green]\n"
            "  1. Create raiconfig.yaml in current directory, or\n"
            "  2. Set up ~/.snowflake/config.toml, or\n"
            "  3. Set up ~/.dbt/profiles.yml, or\n"
            "  4. Pass config programmatically: Config(connections={...})\n\n"
            "[dim italic]See: https://docs.relational.ai/config for examples[/dim italic]"
        )
        parts.append(fix_text)

        diag = Diagnostic(
            name="ConfigFileNotFound",
            message="Could not load config from any source",
            parts=parts,
            severity="error"
        )
        return RichEmitter().emit(diag)


@include_in_docs
class ConfigValidationError(ConfigError, ValueError):
    """Raise when configuration data fails validation.

    This error wraps schema validation failures (typically from Pydantic) and
    includes optional location context and a human-friendly suggestion.

    Parameters
    ----------
    message : str
        Human-readable summary of the validation failure.
    context : :class:`~relationalai.config.errors.ConfigErrorContext`
        Location information (source/file/profile/connection) for the failing config.
    original_error : Exception, optional
        The original exception, if available.
    pydantic_errors : list[ErrorDetails], optional
        Parsed error details from Pydantic.
    suggestion : str, optional
        A suggested fix (for example, a config snippet).

    Attributes
    ----------
    pydantic_errors : list[ErrorDetails]
        Validation errors, when available.
    suggestion : str | None
        A suggested fix, when available.
    """

    def __init__(
        self,
        message: str,
        context: ConfigErrorContext,
        original_error: Exception | None = None,
        pydantic_errors: list[ErrorDetails] | None = None,
        suggestion: str | None = None
    ):
        """
        Initialize validation error.

        Args:
            message: Human-readable summary
            context: Config context (file, profile, connection)
            original_error: Original Pydantic ValidationError (for debugging)
            pydantic_errors: Parsed Pydantic error list from .errors()
            suggestion: Optional suggestion for fixing the error
        """
        super().__init__(message, context)
        self.original_error = original_error
        self.pydantic_errors = pydantic_errors or []
        self.suggestion = suggestion

    def __str__(self) -> str:
        """
        Format as plain text for tracebacks.

        Returns simple text message. Use format_rich() for pretty output.
        """
        lines = [f"Configuration validation failed: {self.message}", ""]

        if self.context:
            lines.append(f"Location: {self.context.format_location()}")
            lines.append("")

        if self.pydantic_errors:
            lines.append("Validation errors:")
            for pyd_err in self.pydantic_errors:
                loc = " → ".join(str(part) for part in pyd_err.get("loc", []))
                msg = pyd_err.get("msg", "Unknown error")
                error_type = pyd_err.get("type", "")
                lines.append(f"  • {loc}")
                lines.append(f"    {msg} (type: {error_type})")
            lines.append("")

        if self.suggestion:
            lines.append("Suggested fix:")
            lines.append(self.suggestion)
            lines.append("")

        return "\n".join(lines)

    def format_rich(self) -> str:
        """Format error with Rich styling (panels, colors, etc.)."""
        parts = []

        # Add location context
        if self.context:
            parts.append(f"[bold cyan]Location:[/bold cyan] [dim]{self.context.format_location()}[/dim]")

        # Add Pydantic error details if available
        if self.pydantic_errors:
            errors_text = "[bold red]Errors:[/bold red]\n"
            for pyd_err in self.pydantic_errors:
                loc = " → ".join(str(part) for part in pyd_err.get("loc", []))
                msg = pyd_err.get("msg", "Unknown error")
                error_type = pyd_err.get("type", "")
                errors_text += f"\n  • [cyan]{loc}[/cyan]\n"
                errors_text += f"    [dim]{msg} (type: {error_type})[/dim]"
            parts.append(errors_text)

        # Add suggestion if available
        if self.suggestion:
            parts.append(f"[bold green]Suggested Fix:[/bold green]\n\n{self.suggestion}")

        diag = Diagnostic(
            name="ConfigValidation",
            message=self.message,
            parts=parts,
            severity="error"
        )
        return RichEmitter().emit(diag)


@include_in_docs
class ConfigFieldMissingError(ConfigValidationError):
    """Raised when a required configuration field is missing.

    This error is raised during config loading/validation when a required key
    (for example ``connections.sf.password``) is absent.

    Parameters
    ----------
    field_path : list[str]
        Path segments to the missing field.
    context : :class:`~relationalai.config.errors.ConfigErrorContext`
        Where the invalid config came from (source, file, profile, connection).
    suggestion : str, optional
        Optional config snippet showing what to add.

    Attributes
    ----------
    field_path : list[str]
        Path segments to the missing field.
    """

    def __init__(
        self,
        field_path: list[str],
        context: ConfigErrorContext,
        suggestion: str | None = None
    ):
        """
        Initialize missing field error.

        Args:
            field_path: Path to missing field (e.g., ['connections', 'snowflake', 'passcode'])
            context: Config context
            suggestion: Optional suggestion for fixing (e.g., config snippet to add)
        """
        self.field_path = field_path

        field_name = " → ".join(field_path)
        message = f"Missing required field: {field_name}"
        super().__init__(message, context, suggestion=suggestion)

    # Inherits __str__ from ConfigValidationError (uses Rich formatting)


@include_in_docs
class ConfigFieldTypeError(ConfigValidationError):
    """Raised when a configuration field has an unexpected type.

    This error is raised during config validation when a field value does not
    match the required type.

    Parameters
    ----------
    field_path : list[str]
        Path segments to the invalid field.
    expected_type : str
        Expected type description (for example ``"type_error.integer"``).
    actual_value : Any
        The value that was provided.
    context : :class:`~relationalai.config.errors.ConfigErrorContext`
        Where the invalid config came from (source, file, profile, connection).

    Attributes
    ----------
    field_path : list[str]
        Path segments to the invalid field.
    expected_type : str
        Expected type description.
    actual_value : Any
        The value that was provided.
    """

    def __init__(
        self,
        field_path: list[str],
        expected_type: str,
        actual_value: Any,
        context: ConfigErrorContext
    ):
        """
        Initialize type error.

        Args:
            field_path: Path to field with wrong type (e.g., ['engine', 'auto_suspend_mins'])
            expected_type: Expected type (e.g., "int", "str")
            actual_value: The actual value that was provided
            context: Config context
        """
        self.field_path = field_path
        self.expected_type = expected_type
        self.actual_value = actual_value

        field_name = " → ".join(field_path)
        message = f"Field '{field_name}' has wrong type (expected {expected_type}, got {type(actual_value).__name__})"
        super().__init__(message, context)

    # Inherits __str__ from ConfigValidationError (uses Rich formatting)
