import importlib
import os
import pkgutil
import sys
import typing as t
from collections import defaultdict
from pathlib import Path
from typing import Optional, OrderedDict

import click
import yaml
from google.protobuf.timestamp_pb2 import Timestamp
from tabulate import tabulate

from clarifai.utils.logging import logger


def from_yaml(filename: str):
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            return yaml.safe_load(f)
    except yaml.YAMLError as e:
        click.echo(f"Error reading YAML file: {e}", err=True)
        return {}


def dump_yaml(data, filename: str):
    try:
        with open(filename, 'w', encoding='utf-8') as f:
            yaml.dump(data, f)
    except Exception as e:
        click.echo(f"Error writing YAML file: {e}", err=True)


def masked_input(prompt='Password: ', mask='*'):
    """Get password input with visual feedback using mask characters.

    Supports common terminal shortcuts:
    - Backspace: Delete one character
    - Ctrl+U: Clear entire line
    - Ctrl+W: Delete last word
    - Ctrl+C: Cancel input

    Args:
        prompt: The prompt to display
        mask: The character to show for each input character (default: '*')

    Returns:
        The entered password string
    """
    import sys

    if os.name == 'nt':  # Windows
        import msvcrt

        click.echo(prompt, nl=False)
        password = ''
        while True:
            ch = msvcrt.getch()
            if not ch:  # EOF
                sys.stdout.write('\r\n')
                sys.stdout.flush()
                break
            if ch in (b'\r', b'\n'):
                sys.stdout.write('\r\n')
                sys.stdout.flush()
                break
            elif ch == b'\x08':  # Backspace
                if password:
                    password = password[:-1]
                    sys.stdout.write('\b \b')
                    sys.stdout.flush()
            elif ch == b'\x15':  # Ctrl+U - Clear line
                if password:
                    sys.stdout.write('\b \b' * len(password))
                    sys.stdout.flush()
                    password = ''
            elif ch == b'\x17':  # Ctrl+W - Delete word
                if password:
                    new_password = password.rstrip()
                    if ' ' in new_password:
                        new_password = new_password[: new_password.rfind(' ') + 1]
                    else:
                        new_password = ''
                    chars_to_delete = len(password) - len(new_password)
                    sys.stdout.write('\b \b' * chars_to_delete)
                    sys.stdout.flush()
                    password = new_password
            elif ch == b'\x03':  # Ctrl+C
                sys.stdout.write('\r\n')
                sys.stdout.flush()
                raise KeyboardInterrupt
            elif ch in (b'\xe0', b'\x00'):  # Special key prefix (arrow keys, function keys)
                # Consume the scan code byte without displaying anything
                msvcrt.getch()
            else:
                decoded = ch.decode('utf-8', errors='ignore')
                if decoded:  # Only add and display if decode produced a character
                    password += decoded
                    sys.stdout.write(mask)
                    sys.stdout.flush()
        return password
    else:  # Unix/Linux/Mac
        # Check if stdin is a terminal (not piped)
        if not sys.stdin.isatty():
            click.echo(prompt, nl=False)
            return sys.stdin.readline().rstrip('\r\n')

        import termios
        import tty

        click.echo(prompt, nl=False)
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            password = ''
            while True:
                ch = sys.stdin.read(1)
                if not ch:  # EOF - terminal connection lost
                    sys.stdout.write('\r\n')
                    sys.stdout.flush()
                    break
                if ch in ('\r', '\n'):
                    sys.stdout.write('\r\n')
                    sys.stdout.flush()
                    break
                elif ch == '\x7f':  # Backspace/Delete
                    if password:
                        password = password[:-1]
                        sys.stdout.write('\b \b')
                        sys.stdout.flush()
                elif ch == '\x15':  # Ctrl+U - Clear line
                    if password:
                        # Clear all asterisks
                        sys.stdout.write('\b \b' * len(password))
                        sys.stdout.flush()
                        password = ''
                elif ch == '\x17':  # Ctrl+W - Delete word
                    if password:
                        # Find last space or delete whole thing
                        new_password = password.rstrip()
                        if ' ' in new_password:
                            new_password = new_password[: new_password.rfind(' ') + 1]
                        else:
                            new_password = ''
                        chars_to_delete = len(password) - len(new_password)
                        sys.stdout.write('\b \b' * chars_to_delete)
                        sys.stdout.flush()
                        password = new_password
                elif ch == '\x03':  # Ctrl+C
                    sys.stdout.write('\r\n')
                    sys.stdout.flush()
                    raise KeyboardInterrupt
                elif ch == '\x1b':  # ESC - start of escape sequence (arrow keys, function keys)
                    # Consume the rest of the escape sequence
                    # Common patterns: \x1b[A (up), \x1b[B (down), \x1b[3~ (delete), etc.
                    next_ch = sys.stdin.read(1)
                    if next_ch == '[':
                        # Read until we hit a letter or ~
                        while True:
                            seq_ch = sys.stdin.read(1)
                            if not seq_ch or seq_ch.isalpha() or seq_ch == '~':
                                break
                    # Ignore the escape sequence, don't add to password
                else:
                    password += ch
                    sys.stdout.write(mask)
                    sys.stdout.flush()
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return password


# Dynamically find and import all command modules from the cli directory
def load_command_modules():
    package_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'cli')

    for _, module_name, _ in pkgutil.iter_modules([package_dir]):
        if module_name not in ['base', '__main__']:  # Skip the base.py and __main__ file itself
            importlib.import_module(f'clarifai.cli.{module_name}')


def display_co_resources(
    response,
    custom_columns={
        'ID': lambda c: c.id,
        'USER_ID': lambda c: c.user_id,
        'DESCRIPTION': lambda c: c.description,
    },
    sort_by_columns=None,
):
    """
    Display compute orchestration resources listing results using rich.

    :param response: Iterable of resource objects to display.
    :param custom_columns: A dictionary mapping column names to extractor functions.
                           Defaults to ID, USER_ID, and DESCRIPTION.
    :param sort_by_columns: Optional list of (column_name, order) tuples specifying sort order.
                            Only column names present in `custom_columns` are considered.
                            Order should be 'asc' or 'desc'.
    :return: None. Prints the formatted table.
    """
    if sort_by_columns:
        for column, order in reversed(sort_by_columns):  # reversed for stable multi-level sort
            if column in custom_columns:
                reverse = order.lower() == 'desc'
                response = sorted(response, key=custom_columns[column], reverse=reverse)

    formatter = TableFormatter(custom_columns)
    print(formatter.format(list(response), fmt="plain"))


class TableFormatter:
    def __init__(self, custom_columns: OrderedDict):
        """
        Initializes the TableFormatter with column headers and custom column mappings.

        :param headers: List of column headers for the table.
        """
        self.custom_columns = custom_columns

    def format(self, objects, fmt='plain'):
        """
        Formats a list of objects into a table with custom columns.

        :param objects: List of objects to format into a table.
        :return: A string representing the table.
        """
        # Prepare the rows by applying the custom column functions to each object
        rows = []
        for obj in objects:
            #   row = [self.custom_columns[header](obj) for header in self.headers]
            row = [f(obj) for f in self.custom_columns.values()]
            rows.append(row)

        # Create the table
        table = tabulate(rows, headers=self.custom_columns.keys(), tablefmt=fmt)
        return table


class AliasedGroup(click.Group):
    def __init__(
        self,
        name: t.Optional[str] = None,
        commands: t.Optional[
            t.Union[t.MutableMapping[str, click.Command], t.Sequence[click.Command]]
        ] = None,
        **attrs: t.Any,
    ) -> None:
        super().__init__(name, commands, **attrs)
        self.alias_map = {}
        self.command_to_aliases = defaultdict(list)

    def add_alias(self, cmd: click.Command, alias: str) -> None:
        self.alias_map[alias] = cmd
        if alias != cmd.name:
            self.command_to_aliases[cmd].append(alias)

    def command(
        self, aliases=None, *args, **kwargs
    ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]:
        cmd_decorator = super().command(*args, **kwargs)
        if aliases is None:
            aliases = []

        def aliased_decorator(f):
            cmd = cmd_decorator(f)
            if cmd.name:
                self.add_alias(cmd, cmd.name)
            for alias in aliases:
                self.add_alias(cmd, alias)
            return cmd

        f = None
        if args and callable(args[0]):
            (f,) = args
        if f is not None:
            return aliased_decorator(f)
        return aliased_decorator

    def group(
        self, aliases=None, *args, **kwargs
    ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]:
        cmd_decorator = super().group(*args, **kwargs)
        if aliases is None:
            aliases = []

        def aliased_decorator(f):
            cmd = cmd_decorator(f)
            if cmd.name:
                self.add_alias(cmd, cmd.name)
            for alias in aliases:
                self.add_alias(cmd, alias)
            return cmd

        f = None
        if args and callable(args[0]):
            (f,) = args
        if f is not None:
            return aliased_decorator(f)
        return aliased_decorator

    def get_command(self, ctx: click.Context, cmd_name: str) -> t.Optional[click.Command]:
        rv = click.Group.get_command(self, ctx, cmd_name)
        if rv is not None:
            return rv
        return self.alias_map.get(cmd_name)

    def format_commands(self, ctx, formatter):
        sub_commands = self.list_commands(ctx)
        limit = formatter.width - 6 - max(len(c) for c in sub_commands) if sub_commands else 80

        # Build command -> (display_name, short_help) map
        cmd_info = {}
        for sub_command in sub_commands:
            cmd = self.get_command(ctx, sub_command)
            if cmd is None or getattr(cmd, 'hidden', False):
                continue
            name = sub_command
            if cmd in self.command_to_aliases:
                aliases = ', '.join(self.command_to_aliases[cmd])
                name = f'{sub_command} ({aliases})'
            help_text = cmd.get_short_help_str(limit=limit)
            cmd_info[sub_command] = (name, help_text)

        if not cmd_info:
            return

        sections = getattr(self, 'command_sections', None)
        if sections:
            listed = set()
            for section_name, cmd_names in sections:
                rows = []
                for cmd_name in cmd_names:
                    if cmd_name in cmd_info:
                        rows.append(cmd_info[cmd_name])
                        listed.add(cmd_name)
                if rows:
                    with formatter.section(section_name):
                        formatter.write_dl(rows)
            remaining = [(n, h) for cn, (n, h) in cmd_info.items() if cn not in listed]
            if remaining:
                with formatter.section("Other"):
                    formatter.write_dl(remaining)
        else:
            rows = list(cmd_info.values())
            with formatter.section("Commands"):
                formatter.write_dl(rows)


class LazyAliasedGroup(AliasedGroup):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.lazy_mapping = {
            'app': 'app',
            'a': 'app',
            'artifact': 'artifact',
            'af': 'artifact',
            'computecluster': 'compute_cluster',
            'cc': 'compute_cluster',
            'deployment': 'deployment',
            'dp': 'deployment',
            'model': 'model',
            'nodepool': 'nodepool',
            'np': 'nodepool',
            'pipeline': 'pipeline',
            'pl': 'pipeline',
            'pipelinerun': 'pipeline_run',
            'pr': 'pipeline_run',
            'pipelinestep': 'pipeline_step',
            'ps': 'pipeline_step',
            'pipelinetemplate': 'pipeline_template',
            'pt': 'pipeline_template',
            'list-instances': 'list_instances',
            'li': 'list_instances',
        }

    def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
        cmd = super().get_command(ctx, cmd_name)
        if cmd is not None:
            return cmd

        if cmd_name in self.lazy_mapping:
            module_name = self.lazy_mapping[cmd_name]
            importlib.import_module(f'clarifai.cli.{module_name}')
            return super().get_command(ctx, cmd_name)

        return None

    def list_commands(self, ctx: click.Context) -> t.List[str]:
        base_commands = super().list_commands(ctx)
        lazy_commands = [k for k in self.lazy_mapping.keys()]
        return sorted(set(base_commands) | set(lazy_commands))


def validate_context(ctx):
    from clarifai.utils.logging import logger

    if ctx.obj == {}:
        logger.error("CLI config file missing. Run `clarifai login` to set up the CLI config.")
        sys.exit(1)


def validate_context_auth(pat: str, user_id: str, api_base: str = None):
    """
    Validate a Personal Access Token (PAT) by making a test API call.

    Args:
        pat (str): The Personal Access Token to validate
        user_id (str): The user ID associated with the token
        api_base (str): The API base URL. Defaults to None (uses default).
    """
    try:
        from clarifai_grpc.grpc.api.status import status_code_pb2

        from clarifai.client.user import User

        logger.debug("Validating the Context Credentials...")

        # Create user client for validation
        if api_base:
            user_client = User(user_id=user_id, pat=pat, base_url=api_base)
        else:
            user_client = User(user_id=user_id, pat=pat)

        # Try to get user info as a test API call
        response = user_client.get_user_info()

        if response.status.code == status_code_pb2.SUCCESS:
            logger.debug("✅ Context is valid")

    except Exception as e:
        # Check for common authentication errors and provide user-friendly messages
        logger.error("❌ Authentication failed. Please check your token and user ID.")
        raise click.Abort()  # Exit without saving the configuration


def customize_ollama_model(
    model_path, user_id, model_name=None, port=None, context_length=None, verbose=False
):
    """Customize the Ollama model name in the cloned template files.
    Args:
     model_path: Path to the cloned model directory
     model_name: The model name to set (e.g., 'llama3.1', 'mistral') - optional
     port: Port for Ollama server - optional
     context_length: Context length for the model - optional
     verbose: Whether to enable verbose logging - optional (defaults to False)

    """
    config_path = os.path.join(model_path, 'config.yaml')
    if os.path.exists(config_path):
        with open(config_path, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)

        if 'toolkit' not in config or config['toolkit'] is None:
            config['toolkit'] = {}
        if model_name is not None:
            config['toolkit']['model'] = model_name
        if port is not None:
            config['toolkit']['port'] = port
        if context_length is not None:
            config['toolkit']['context_length'] = context_length
        with open(config_path, 'w', encoding='utf-8') as f:
            yaml.dump(config, f, default_flow_style=False, sort_keys=False)

        # Simplify the cloned config (remove placeholder user_id/app_id, convert to compute.instance)
        simplify_cloned_config(config_path, model_name=model_name)

    model_py_path = os.path.join(model_path, "1", "model.py")

    if not os.path.exists(model_py_path):
        logger.warning(f"Model file {model_py_path} not found, skipping model name customization")
        return

    try:
        # Read the model.py file
        with open(model_py_path, 'r', encoding='utf-8') as file:
            content = file.read()
        if model_name:
            # Replace the default model name in the load_model method
            content = content.replace(
                'self.model = os.environ.get("OLLAMA_MODEL_NAME", \'llama3.2\')',
                f'self.model = os.environ.get("OLLAMA_MODEL_NAME", \'{model_name}\')',
            )

        if port:
            # Replace the default port variable in the model.py file
            content = content.replace("PORT = '23333'", f"PORT = '{port}'")

        if context_length:
            # Replace the default context length variable in the model.py file
            content = content.replace(
                "context_length = '8192'", f"context_length = '{context_length}'"
            )

        verbose_str = str(verbose)
        if "VERBOSE_OLLAMA = True" in content:
            content = content.replace("VERBOSE_OLLAMA = True", f"VERBOSE_OLLAMA = {verbose_str}")
        elif "VERBOSE_OLLAMA = False" in content:
            content = content.replace("VERBOSE_OLLAMA = False", f"VERBOSE_OLLAMA = {verbose_str}")

        # Write the modified content back to model.py
        with open(model_py_path, 'w', encoding='utf-8') as file:
            file.write(content)

    except Exception as e:
        logger.error(f"Failed to customize Ollama model name in {model_py_path}: {e}")
        raise


def check_ollama_installed():
    """Check if the Ollama CLI is installed."""
    try:
        import subprocess

        result = subprocess.run(
            ['ollama', '--version'], capture_output=True, text=True, check=False
        )
        if result.returncode == 0:
            return True
        else:
            return False
    except FileNotFoundError:
        return False


def check_lmstudio_installed():
    """Check if the LM Studio CLI is installed."""
    try:
        import subprocess

        result = subprocess.run(['lms', 'version'], capture_output=True, text=True, check=False)
        if result.returncode == 0:
            return True
        else:
            return False
    except FileNotFoundError:
        return False


def _is_package_installed(package_name):
    """Helper function to check if a single package in requirements.txt is installed."""
    import importlib.metadata

    try:
        importlib.metadata.distribution(package_name)
        logger.debug(f"✅ {package_name} - installed")
        return True
    except importlib.metadata.PackageNotFoundError:
        logger.debug(f"❌ {package_name} - not installed")
        return False
    except Exception as e:
        logger.warning(f"Error checking {package_name}: {e}")
        return False


def parse_requirements(model_path: str):
    """Parse requirements.txt in the model directory and return a dictionary of dependencies."""
    from packaging.requirements import Requirement

    requirements_path = Path(model_path) / "requirements.txt"

    if not requirements_path.exists():
        logger.warning(f"requirements.txt not found at {requirements_path}")
        return []

    deps = {}
    for line in requirements_path.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        try:
            req = Requirement(line)
            deps[req.name] = str(req.specifier) if req.specifier else None
        except Exception as e:
            logger.warning(f"⚠️ Could not parse line: {line!r} — {e}")
    return deps


def check_requirements_installed(model_path: str = None, dependencies: dict = None):
    """Check if all dependencies in requirements.txt are installed.
    Args:
        model_path: Path to the model directory
        dependencies: Dictionary of dependencies
    Returns:
        True if all dependencies are installed, False otherwise
    """

    try:
        if not dependencies:
            if not model_path:
                logger.error("No model_path or dependencies provided to check requirements.")
                return False
            dependencies = parse_requirements(model_path)
        missing = [
            f"{package_name}{full_req}" if full_req else package_name
            for package_name, full_req in dependencies.items()
            if not _is_package_installed(package_name)
        ]

        if not missing:
            logger.info(f"✅ All {len(dependencies)} dependencies are installed!")
            return True

        # Report missing packages
        logger.error(
            f"❌ {len(missing)} of {len(dependencies)} required packages are missing in the current environment"
        )
        logger.error("\n".join(f"  - {pkg}" for pkg in missing))
        if model_path:
            try:
                requirements_path = (Path(model_path) / "requirements.txt").relative_to(Path.cwd())
            except ValueError:
                requirements_path = Path(model_path) / "requirements.txt"
            logger.warning(f"To install: pip install -r {requirements_path}")
        logger.warning(
            "Tip: use '--mode env' to auto-install deps in a virtualenv, "
            "or '--mode container' to run in Docker."
        )
        return False

    except Exception as e:
        logger.error(f"Failed to check requirements: {e}")
        return False


def convert_timestamp_to_string(timestamp: Timestamp) -> str:
    """Converts a Timestamp object to a string.

    Args:
        timestamp (Timestamp): The Timestamp object to convert.

    Returns:
        str: The converted string in ISO 8601 format.
    """
    if not timestamp:
        return ""

    datetime_obj = timestamp.ToDatetime()

    return datetime_obj.strftime('%Y-%m-%dT%H:%M:%SZ')


def customize_huggingface_model(model_path, user_id, model_name):
    config_path = os.path.join(model_path, 'config.yaml')
    if os.path.exists(config_path):
        with open(config_path, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)

        if model_name:
            # Update the repo_id in checkpoints section
            if 'checkpoints' not in config:
                config['checkpoints'] = {}
            config['checkpoints']['repo_id'] = model_name

        with open(config_path, 'w', encoding='utf-8') as f:
            yaml.dump(config, f, default_flow_style=False, sort_keys=False)

        # Simplify the cloned config to use compute.instance shorthand
        simplify_cloned_config(config_path, model_name=model_name)

        logger.info(f"Updated Hugging Face model repo_id to: {model_name}")
    else:
        logger.warning(f"config.yaml not found at {config_path}, skipping model configuration")


def simplify_cloned_config(config_path, user_id=None, model_name=None):
    """Post-process a cloned config.yaml to simplified format.

    - Removes user_id/app_id placeholders (will be injected from CLI context at deploy time)
    - Converts inference_compute_info to compute.instance shorthand (if it matches a preset)
    - Keeps model.id, model_type_id, checkpoints, build_info as-is
    """
    if not os.path.exists(config_path):
        return

    with open(config_path, 'r', encoding='utf-8') as f:
        config = yaml.safe_load(f)

    if not config:
        return

    # Remove placeholder user_id/app_id from model section
    model = config.get('model', {})
    placeholder_values = {'user_id', 'YOUR_USER_ID', 'app_id', 'YOUR_APP_ID', ''}
    if model.get('user_id') in placeholder_values:
        model.pop('user_id', None)
    if model.get('app_id') in placeholder_values:
        model.pop('app_id', None)

    # Convert inference_compute_info to compute.instance shorthand
    if 'inference_compute_info' in config and 'compute' not in config:
        from clarifai.utils.compute_presets import infer_gpu_from_config

        gpu_name = infer_gpu_from_config(config)
        if gpu_name:
            config['compute'] = {'instance': gpu_name}
            del config['inference_compute_info']

    # Remove placeholder hf_token values from checkpoints
    checkpoints = config.get('checkpoints', {})
    if checkpoints:
        hf_token = checkpoints.get('hf_token', '')
        if hf_token in {'your_hf_token', 'hf_token', 'your-huggingface-token', ''}:
            checkpoints.pop('hf_token', None)

    # Update model_id from directory name if it's a placeholder
    if model_name:
        # Use the last part of the model_name (e.g. 'Llama-3-8B' from 'meta-llama/Llama-3-8B')
        simple_name = model_name.split('/')[-1] if '/' in model_name else model_name
        model['id'] = simple_name

    config['model'] = model

    with open(config_path, 'w', encoding='utf-8') as f:
        yaml.dump(config, f, default_flow_style=False, sort_keys=False)


def customize_lmstudio_model(model_path, user_id, model_name=None, port=None, context_length=None):
    """Customize the LM Studio model name in the cloned template files.
    Args:
     model_path: Path to the cloned model directory
     model_name: The model name to set (e.g., 'qwen/qwen3-4b-thinking-2507') - optional
     port: Port for LM Studio server - optional
     context_length: Context length for the model - optional

    """
    config_path = os.path.join(model_path, 'config.yaml')

    if os.path.exists(config_path):
        with open(config_path, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)
        if 'toolkit' not in config or config['toolkit'] is None:
            config['toolkit'] = {}
        if model_name is not None:
            config['toolkit']['model'] = model_name
        if port is not None:
            config['toolkit']['port'] = port
        if context_length is not None:
            config['toolkit']['context_length'] = context_length
        with open(config_path, 'w', encoding='utf-8') as f:
            yaml.dump(config, f, default_flow_style=False, sort_keys=False)

        # Simplify the cloned config (remove placeholder user_id/app_id, convert to compute.instance)
        simplify_cloned_config(config_path, model_name=model_name)

        logger.info(f"Updated LM Studio model configuration in: {config_path}")
    else:
        logger.warning(f"config.yaml not found at {config_path}, skipping model configuration")


def prompt_required_field(message: str, default: Optional[str] = None) -> str:
    """Prompt the user for a required field, optionally with a default.
    Used inside the model CLI to prompt the user for required fields if config.yaml is missing.
    Args:
        message (str): The message to display to the user.
        default (Optional[str]): The default value to use if the user does not enter a value.

    Returns:
        str: The value entered by the user.
    """
    while True:
        prompt = f"{message}"
        if default:
            prompt += f" [{default}]"
        prompt += ": "
        value = input(prompt).strip()
        if not value and default:
            return default
        if value:
            return value
        click.echo("❌ This field is required. Please enter a value.")


def prompt_optional_field(message: str, default: Optional[str] = None) -> Optional[str]:
    """Prompt the user for an optional field.
    Used inside the model CLI to prompt the user for optional fields if config.yaml is missing.
    Args:
        message (str): The message to display to the user.
        default (Optional[str]): The default value to use if the user does not enter a value.

    Returns:
        Optional[str]: The value entered by the user.
    """
    prompt = f"{message}"
    if default:
        prompt += f" [{default}]"
    prompt += ": "
    value = input(prompt).strip()
    if not value:
        return default
    return value


def prompt_int_field(message: str, default: Optional[int] = None) -> int:
    """Prompt the user for an integer field (required).
    Used inside the model CLI to prompt the user for integer fields if config.yaml is missing.
    Args:
        message (str): The message to display to the user.
        default (Optional[int]): The default value to use if the user does not enter a value.

    Returns:
        int: The value entered by the user.
    """
    while True:
        prompt = f"{message}"
        if default is not None:
            prompt += f" [{default}]"
        prompt += ": "
        raw = input(prompt).strip()
        if not raw and default is not None:
            return default
        try:
            return int(raw)
        except ValueError:
            click.echo("❌ Please enter a valid integer.")


def prompt_yes_no(message: str, default: Optional[bool] = None) -> bool:
    """Prompt the user for a yes/no decision.
    Used inside the model CLI to prompt the user for yes/no fields if config.yaml is missing.
    Args:
        message (str): The message to display to the user.
        default (Optional[bool]): The default value to use if the user does not enter a value.

    Returns:
        bool: The value entered by the user.
    """
    if default is True:
        suffix = " [Y/n]"
    elif default is False:
        suffix = " [y/N]"
    else:
        suffix = " [y/n]"
    prompt = f"{message}{suffix}: "
    while True:
        response = input(prompt).strip().lower()
        if not response and default is not None:
            return default
        if response in ("y", "yes"):
            return True
        if response in ("n", "no"):
            return False
        click.echo("❌ Please respond with 'y' or 'n'.")


def print_section(title: str, description: str, note: Optional[str] = None) -> None:
    """Print a section with a title, description, and note.
    Used inside the model CLI to print sections if config.yaml is missing.
    Args:
        title (str): The title of the section.
        description (str): The description of the section.
        note (Optional[str]): The note to display below the section.
    """
    click.echo()
    click.echo(click.style(title, fg="bright_cyan", bold=True))
    if description:
        click.echo(description)
    if note:
        click.echo(click.style(note, fg="yellow"))


def print_field_help(name: str, description: str) -> None:
    """Print a field with a name and description.
    Used inside the model CLI to print fields if config.yaml is missing.
    Args:
        name (str): The name of the field.
        description (str): The description of the field.
    """
    click.echo(click.style(f"➤ {name}", fg="bright_green", bold=True))
    if description:
        click.echo(click.style(f"    {description}", fg="green"))
