# Copyright (c) CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
#

"""
Console interaction and formatting utilities.

This module provides functions for:
- User input and prompting
- Console output formatting
- Converting input types
- Printing data structures
"""

import importlib.resources as pkg_resources
import json
import sys
from ast import literal_eval
from string import Template
from typing import Any

import pyaml
from rich import print
from rich.console import Console
from rich.panel import Panel

# Initialize Rich console with full terminal width
console = Console(width=None)
error_console = Console(stderr=True, style="bold red", width=None)


def mask_secret(value: str | None, show_chars: int = 4) -> str | None:
    """
    Mask a secret value showing only the last few characters.

    Args:
        value: The secret value to mask
        show_chars: Number of characters to show at the end

    Returns:
        Masked string like '****abcd' or None if value is empty/None
    """
    if not value or not value.strip():
        return None

    if len(value) <= show_chars:
        # If value is too short, show all asterisks
        return "*" * len(value)

    return "*" * (len(value) - show_chars) + value[-show_chars:]


def get_input(question: str, default: str | None = None) -> str:
    """
    Get input from user with optional default value.

    Args:
        question: The prompt to display to the user
        default: Optional default value if user enters nothing

    Returns:
        User input or default value
    """
    if default:
        question = f"{question} [{default}]: "
    else:
        question += ": "

    result = input(question).strip()
    if result:
        return result
    else:
        return default


def get_required_input(prompt_text: str, default: str | None = None, display_default: str | None = None) -> str:
    """
    Get required input from user, reprompt until input is provided.

    Args:
        prompt_text: The prompt to display to the user
        default: Optional default value to use when user presses Enter
        display_default: Optional display value to show in prompt (for masking secrets)

    Returns:
        Non-empty user input
    """
    # Use display_default for the prompt if provided, otherwise use default
    prompt_default = display_default if display_default is not None else default
    input_text = get_input(prompt_text, default=prompt_default)

    # If user just pressed Enter and we have a real default, use it
    if not input_text and default:
        input_text = default

    while not input_text:
        print(f" - {prompt_text} must not be empty. (ctrl-c to exit)")
        input_text = get_input(prompt_text)
    return input_text


def get_choice(
    choices: dict | list,
    label: str,
    key_id: str | None = None,
    value_id: str | None = None,
    default: str | None = None,
    custom_label: bool = False,
) -> Any:
    """
    Get user choice from a dict or a list of dicts.

    Args:
        choices: Choices as a dictionary or list of dictionaries
        label: Unique identifier of the choice to show the user
        key_id: The id for a key in a choices dict entry
        value_id: The id for the value field in a choices dict entry. If none, returns list item
        default: A default value to use for this choice
        custom_label: Override the prompt completely with a custom label

    Returns:
        The value of the entry chosen or a user entered custom value
    """
    choice = None
    if custom_label:
        print(label)
    else:
        print(f"Please choose which {label} you wish to use")
    for ix, key in enumerate(choices, start=1):
        if not key_id:
            print(f"  {ix:2>}. {key}")
        else:
            if value_id:
                print(f"  {ix:2>}. {key.get(key_id):30} : {key.get(value_id)}")
            else:
                print(f"  {ix:2>}. {key.get(key_id)}")
    response = get_input("Choice", default=default)

    # response can either be an input choice OR an existing key
    if not str(response).isdigit():
        choice = response
    else:
        try:
            key_to_use = int(response) - 1
            if not key_id:
                if isinstance(choices, list):
                    choice = choices[key_to_use]
                elif isinstance(choices, dict):
                    choice = choices[list(choices.keys())[key_to_use]]
            else:
                choice = choices[key_to_use].get(value_id) if value_id else choices[key_to_use]
        except (ValueError, IndexError, TypeError):
            choice = None

    return choice


def confirm(prompt: str | None = None, resp: bool = False) -> bool:
    """
    Prompt for yes or no response from the user.

    Args:
        prompt: The prompt to display to the user
        resp: Default response if user just presses Enter

    Returns:
        True for yes and False for no
    """
    if prompt is None:
        prompt = "Confirm"

    prompt = "{} [{}]|{} ".format(prompt, "y", "n") if resp else "{} [{}]|{} ".format(prompt, "n", "y")

    while True:
        ans = get_input(prompt)
        if not ans:
            return resp
        if ans not in ["y", "Y", "n", "N"]:
            print("please enter y or n.")
            continue
        if ans == "y" or ans == "Y":
            return True
        if ans == "n" or ans == "N":
            return False


def print_dict(input_dict: dict, _indent: int = 0) -> None:
    """
    Pretty print a dictionary to stdout.

    Args:
        input_dict: Dictionary to print
        indent: Indentation level
    """
    top_keys = {k: v for k, v in input_dict.items() if not isinstance(v, dict | list | set)}
    other_keys = {k: v for k, v in input_dict.items() if isinstance(v, dict | list | set)}
    pyaml.dump(top_keys, sys.stdout)
    pyaml.dump(other_keys, sys.stdout)


def eprint(*args, **kwargs) -> None:
    """
    Print to stderr using Rich's error console.

    Args:
        *args: Arguments to print
        **kwargs: Keyword arguments for print function
    """
    # Remove 'file' from kwargs if present as Rich uses stderr=True when creating the console
    if "file" in kwargs:
        del kwargs["file"]
    error_console.print(*args, **kwargs)


def print_info(message: str) -> None:
    """
    Print an info message in blue.

    Args:
        message: Message to print
    """
    console.print(message, style="blue")


def print_success(message: str) -> None:
    """
    Print a success message in green.

    Args:
        message: Message to print
    """
    console.print(message, style="green")


def print_warning(message: str) -> None:
    """
    Print a warning message in yellow.

    Args:
        message: Message to print
    """
    console.print(message, style="yellow")


def yes_or_no(test_input: Any, yes_color: str | None = None, no_color: str | None = None, format: str | None = None) -> str:
    """
    Convert a boolean value to "yes" or "no" with optional formatting.

    Args:
        test_input: Boolean value to convert
        yes_color: Optional Rich style for "yes"
        no_color: Optional Rich style for "no"
        format: Optional format style (UPPER, LOWER, CAP)

    Returns:
        Formatted "yes" or "no" string
    """
    response_values = ("yes", "no")
    if format == "UPPER":
        yes, no = (x.upper() for x in response_values)
    elif format == "LOWER":
        yes, no = (x.lower() for x in response_values)
    elif format == "CAP":
        yes, no = (x.capitalize() for x in response_values)
    else:
        yes, no = response_values

    evaluated = bool(literal_eval(str(test_input).capitalize()))

    if yes_color and no_color:
        return f"[{yes_color}]{yes}[/]" if evaluated else f"[{no_color}]{no}[/]"
    else:
        return yes if evaluated else no


def yes_or_no_maybe(yes_no_test: bool, maybe_test: bool, yes_color: str, no_color: str, maybe_color: str, format: str | None = None) -> str:
    """
    Convert values to "yes", "no", or "maybe" with color formatting.

    Args:
        yes_no_test: Boolean to determine yes/no
        maybe_test: Boolean to determine maybe
        yes_color: Rich style for "yes"
        no_color: Rich style for "no"
        maybe_color: Rich style for "maybe"
        format: Optional format style (UPPER, LOWER, CAP)

    Returns:
        Formatted "yes", "no", or "maybe" string
    """
    response_values = ("yes", "no", "maybe")
    if format == "UPPER":
        yes, no, maybe = (x.upper() for x in response_values)
    elif format == "LOWER":
        yes, no, maybe = (x.lower() for x in response_values)
    elif format == "CAP":
        yes, no, maybe = (x.capitalize() for x in response_values)
    else:
        yes, no, maybe = response_values

    evaluated = bool(literal_eval(str(yes_no_test).capitalize()))

    if evaluated:
        return f"[{yes_color}]{yes}[/]"
    if not evaluated and maybe_test:
        return f"[{maybe_color}]{maybe}[/]"
    else:
        return f"[{no_color}]{no}[/]"


def render_template(template_string: str, replacement_map: dict[str, str]) -> str:
    """
    Render a template string with replacements.

    Args:
        template_string: Template string with $placeholders
        replacement_map: Dictionary of replacements

    Returns:
        Rendered template string
    """
    prepared_template = Template(template_string)
    return prepared_template.safe_substitute(**replacement_map)


def get_error_resource_path(resource_name: str, subfolder: str = "errors"):
    """
    Get the path to an error resource file.

    Args:
        resource_name: Name of the resource file.
        subfolder: Subfolder within data directory. Defaults to "errors".

    Returns:
        pathlib.Path: Path to the requested resource.
    """
    # Using importlib.resources (imported as pkg_resources at the top)
    # This is the modern approach and not the deprecated pkg_resources from setuptools
    return pkg_resources.files("src") / f"data/{subfolder}/{resource_name}"


def print_error_image(context: str | None = None, image_name: str | None = None, error_type: str | None = None) -> bool:
    """
    Displays an error image in the terminal with optional context.
    Uses term-image to render JPG files directly in the terminal with block style.

    Args:
        context: Additional error context to display
        image_name: Specific error image to use, defaults to manifest setting
        error_type: Type of error to display image for

    Returns:
        True if image was displayed successfully, False otherwise
    """
    try:
        import term_image.image

        # Get manifest path and verify existence
        error_manifest_path = get_error_resource_path("manifest.json")

        if not error_manifest_path.exists():
            print("[ERROR] Error manifest not found!")
            return False

        # Load manifest
        error_manifest = json.load(error_manifest_path.open())

        # Select image based on error_type or use default/specified
        if error_type and error_manifest.get("error_types", {}).get(error_type):
            error_file = error_manifest["error_types"][error_type]
        else:
            error_file = image_name or error_manifest.get("error_file", "error.jpg")

        # Get path to error image
        error_file_path = get_error_resource_path(error_file)

        if not error_file_path.exists():
            print(f"[ERROR] Error image file '{error_file}' not found!")
            return False

        # Configure terminal image renderer
        term_img = term_image.image.from_file(str(error_file_path))
        term_img.height = 30  # Adjust height as needed
        term_img.draw(h_align="left", v_align="bottom", pad_height=0, pad_width=0)

        # Display additional context if provided
        if context:
            console.print(Panel(f"{context}", title="Error Details", border_style="red"))

        return True

    except Exception as e:
        print(f"Failed to display error information: {str(e)}")
        return False


def markdown_format(input_data: Any) -> str:
    """
    Format input into nice markdown accordingly.

    Dict -> formatted list of key/value pairs
    Everything Else -> Str

    Args:
        input_data: Data to format

    Returns:
        str: Formatted markdown string
    """
    if isinstance(input_data, dict):
        return "\n".join([f"*{k}*: {v}" for k, v in input_data.items()])
    else:
        return input_data


def format_account_stage(stage):
    """
    Format account stage with appropriate colors.

    Args:
        stage (str): The account stage to format.

    Returns:
        str: Formatted account stage with Rich formatting tags.
    """
    if not stage:
        return stage

    stage_lower = stage.lower()

    # Define stage mappings with fuzzy matching keywords
    stage_formats = {
        "closed": {"style": "[red]", "keywords": ["closed", "lost", "closed lost"]},
        "churned": {"style": "[hot_pink]", "keywords": ["churn", "churned customer", "former"]},
        "customer": {"style": "[bold cornflower_blue]", "keywords": ["customer", "client"]},
        "prospect": {"style": "[bold green]", "keywords": ["prospect", "lead"]},
        "pipeline": {"style": "[aquamarine1]", "keywords": ["pipeline", "in progress"]},
        "competitor": {"style": "[grey30]", "keywords": ["competitor", "lost to competitor"]},
    }

    # Find matching stage format
    for format_info in stage_formats.values():
        if any(keyword in stage_lower for keyword in format_info["keywords"]):
            return f"{format_info['style']}{stage}[/]"

    return stage
