"""Output functionality."""

from __future__ import annotations

import logging
import os
import sys
import textwrap

from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, TypeVar


if TYPE_CHECKING:
    from .utils import TermFeatures


T = TypeVar("T", bound="Level")


class Color:
    """Color constants."""

    BLACK = "\033[30m"
    RED = "\033[31m"
    GREEN = "\033[32m"
    YELLOW = "\033[33m"
    BLUE = "\033[34m"
    MAGENTA = "\033[35m"
    CYAN = "\033[36m"
    WHITE = "\033[37m"
    GREY = "\033[90m"  # Bright black?
    BRIGHT_RED = "\033[91m"
    BRIGHT_GREEN = "\033[92m"
    BRIGHT_YELLOW = "\033[93m"
    BRIGHT_BLUE = "\033[94m"
    BRIGHT_MAGENTA = "\033[95m"
    BRIGHT_CYAN = "\033[96m"
    BRIGHT_WHITE = "\033[97m"
    END = "\033[0m"


class Level(Enum):
    """An exit message prefix."""

    CRITICAL = "Critical"
    DEBUG = "Debug"
    ERROR = "Error"
    HINT = "Hint"
    INFO = "Info"
    NOTE = "Note"
    WARNING = "Warning"

    @property
    def log_level(self: Level) -> int:
        """Return a log level.

        :returns: The log level
        """
        mapping = {
            Level.CRITICAL: logging.CRITICAL,
            Level.DEBUG: logging.DEBUG,
            Level.ERROR: logging.ERROR,
            Level.HINT: logging.INFO,
            Level.INFO: logging.INFO,
            Level.NOTE: logging.INFO,
            Level.WARNING: logging.WARNING,
        }
        return mapping[self]

    @classmethod
    def _longest_name(cls: type[T]) -> int:
        """Return the longest exit message prefix.

        :returns: The longest exit message prefix
        """
        return max(len(member.value) for member in cls)

    @classmethod
    def longest_formatted(cls: type[T]) -> int:
        """Return the longest exit message prefix.

        :returns: The longest exit message prefix
        """
        return max(len(str(member)) for member in cls)

    def __str__(self: Level) -> str:
        """Return the exit message prefix as a string.

        :returns: The exit message prefix as a string
        """
        return (
            f"{' ' * (self._longest_name() - len(self.name))}{self.name.capitalize()}: "
        )


@dataclass
class Msg:
    """An object to hold a message to present when exiting."""

    #: The message that will be presented
    message: str
    #: The prefix for the message, used for formatting
    prefix: Level = Level.ERROR

    @property
    def color(self: Msg) -> str:
        """Return a color for the prefix.

        :returns: The color for the prefix
        """
        color_mapping = {
            Level.CRITICAL: Color.BRIGHT_RED,
            Level.DEBUG: Color.GREY,
            Level.ERROR: Color.RED,
            Level.HINT: Color.CYAN,
            Level.INFO: Color.MAGENTA,
            Level.NOTE: Color.GREEN,
            Level.WARNING: Color.YELLOW,
        }
        return color_mapping[self.prefix]

    def to_lines(
        self: Msg,
        color: bool,  # noqa: FBT001
        width: int,
        with_prefix: bool,  # noqa: FBT001
    ) -> list[str]:
        """Output exit message to the console.

        :param color: Whether to color the message
        :param width: Constrain message to width
        :param with_prefix: Whether to prefix the message
        :returns: The exit message as a string
        """
        prefix_length = Level.longest_formatted()
        indent = " " * prefix_length

        lines = []
        message_lines = self.message.splitlines()

        lines.extend(
            textwrap.fill(
                message_lines[0],
                width=width,
                break_on_hyphens=False,
                initial_indent=str(self.prefix) if with_prefix else indent,
                subsequent_indent=indent,
            ).splitlines(),
        )

        if len(message_lines) > 1:
            for line in message_lines[1:]:
                lines.extend(
                    textwrap.fill(
                        line,
                        width=width,
                        break_on_hyphens=False,
                        initial_indent=indent,
                        subsequent_indent=indent,
                    ).splitlines(),
                )

        start_color = self.color if color else ""
        end_color = Color.END if color else ""

        return [f"{start_color}{line}{end_color}" for line in lines]


class Output:
    """Output functionality."""

    def __init__(  # noqa: PLR0913
        self: Output,
        log_file: str,
        log_level: str,
        log_append: str,
        term_features: TermFeatures,
        verbosity: int,
    ) -> None:
        """Initialize the output object.

        Args:
            log_file: The path to the los.get_terminal_size()og file
            log_level: The log level
            log_append: Whether to append to the log file
            term_features: Terminal features
            verbosity: The verbosity level
        """
        self._verbosity = verbosity
        self.call_count: dict[str, int] = {
            "critical": 0,
            "debug": 0,
            "error": 0,
            "hint": 0,
            "info": 0,
            "note": 0,
            "warning": 0,
        }
        self.term_features = term_features
        self.logger = logging.getLogger("pip4a")
        if log_level != "notset":
            self.logger.setLevel(log_level.upper())
            self.log_to_file = bool(log_file)
            log_file_path = Path(log_file)
            if log_file_path.exists() and log_append == "false":
                log_file_path.unlink()
            formatter = logging.Formatter(
                fmt="%(asctime)s %(levelname)s '%(name)s.%(module)s.%(funcName)s' %(message)s",
            )
            handler = logging.FileHandler(log_file)
            handler.setFormatter(formatter)
            handler.setLevel(log_level.upper())
            self.logger.addHandler(handler)
            self.log_to_file = True
        else:
            self.log_to_file = False

    def critical(self: Output, msg: str) -> None:
        """Print a critical message to the console.

        :param msg: The message to print
        """
        self.call_count["critical"] += 1
        self.log(msg, level=Level.CRITICAL)
        sys.exit(1)

    def debug(self: Output, msg: str) -> None:
        """Print a debug message to the console.

        :param msg: The message to print
        """
        self.call_count["debug"] += 1
        self.log(msg, level=Level.DEBUG)

    def error(self: Output, msg: str) -> None:
        """Print an error message to the console.

        :param msg: The message to print
        """
        self.call_count["error"] += 1
        self.log(msg, level=Level.ERROR)

    def hint(self: Output, msg: str) -> None:
        """Print a hint message to the console.

        :param msg: The message to print
        """
        self.call_count["hint"] += 1
        self.log(msg, level=Level.HINT)

    def info(self: Output, msg: str) -> None:
        """Print a hint message to the console.

        :param msg: The message to print
        """
        self.call_count["info"] += 1
        self.log(msg, level=Level.INFO)

    def note(self: Output, msg: str) -> None:
        """Print a note message to the console.

        :param msg: The message to print
        """
        self.call_count["note"] += 1
        self.log(msg, level=Level.NOTE)

    def warning(self: Output, msg: str) -> None:
        """Print a warning message to the console.

        :param msg: The message to print
        """
        self.call_count["warning"] += 1
        self.log(msg, level=Level.WARNING)

    def log(self: Output, msg: str, level: Level = Level.ERROR) -> None:
        """Print a message to the console.

        :param msg: The message to print
        :param prefix: The prefix for the message
        """
        if self.log_to_file:
            self.logger.log(level.log_level, msg, stacklevel=3)

        try:
            width = float(os.get_terminal_size()[0])
        except OSError:
            width = 80
        narrow = 80
        wide = 120
        if width <= narrow:
            set_width = width - 2
        elif wide >= width >= narrow:
            set_width = width * 0.9
        elif width > wide:
            set_width = width * 0.8

        debug = 2
        info = 1
        if (self._verbosity < debug and level == Level.DEBUG) or (
            self._verbosity < info and level == Level.INFO
        ):
            return

        lines = Msg(message=msg, prefix=level).to_lines(
            color=self.term_features.color,
            width=int(set_width),
            with_prefix=True,
        )
        if level in (Level.CRITICAL, Level.ERROR):
            print("\n".join(lines), file=sys.stderr)  # noqa: T201
        else:
            print("\n".join(lines))  # noqa: T201
