import importlib
import logging
import os
import sys
import warnings
from pathlib import Path
from typing import Optional

import click

from highlighter.cli.cli_logging import configure_root_logger
from highlighter.core.config import (
    HighlighterRuntimeConfig,
    HighlighterRuntimeConfigError,
)
from highlighter.version import __version__

_DEFAULT_PROFILES_YAML = Path.home() / ".highlighter-profiles.yaml"

# Subcommand modules are imported on demand so that heavy dependencies
# (numpy, pandas, boto3, …) are only loaded when the specific subcommand runs.
_LAZY_SUBCOMMANDS = {
    "agent": ("highlighter.cli.agent", "agent_group"),
    "assessment": ("highlighter.cli.assessment", "assessment_group"),
    "case": ("highlighter.cli.case", "case_group"),
    "config": ("highlighter.cli.config", "config_group"),
    "data-file": ("highlighter.cli.data_files", "data_file_group"),
    "file": ("highlighter.cli.data_files", "data_file_group"),
    "dataset": ("highlighter.cli.dataset", "dataset_group"),
    "datasource": ("highlighter.cli.datasource", "datasource_group"),
    "entity": ("highlighter.cli.entity", "entity_group"),
    "evaluation": ("highlighter.cli.evaluation", "evaluation_group"),
    "experiment": ("highlighter.cli.experiment", "experiment_group"),
    "generate": ("highlighter.cli.template", "generate_group"),
    "object-class": ("highlighter.cli.object_class", "object_class_group"),
    "pipeline-instance": ("highlighter.cli.pipeline_instance", "pipeline_instance_group"),
    "step": ("highlighter.cli.step", "step_group"),
    "task": ("highlighter.cli.task", "task_group"),
    "train": ("highlighter.cli.trainer", "train_group"),
    "training-run": ("highlighter.cli.training_run", "training_run_group"),
    "user": ("highlighter.cli.user", "user_group"),
    "workflow": ("highlighter.cli.workflow", "workflow_group"),
    "workflow-order": ("highlighter.cli.workflow_order", "workflow_order_group"),
}


class LazyGroup(click.Group):
    """Click group that imports subcommand modules on first use."""

    def __init__(self, *args, lazy_subcommands=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._lazy_subcommands = lazy_subcommands or {}

    def list_commands(self, ctx):
        base = super().list_commands(ctx)
        lazy = sorted(set(self._lazy_subcommands.keys()) - set(base))
        return base + lazy

    def get_command(self, ctx, cmd_name):
        if cmd_name in self._lazy_subcommands and cmd_name not in self.commands:
            return self._resolve_lazy(cmd_name)
        return super().get_command(ctx, cmd_name)

    def _resolve_lazy(self, cmd_name):
        mod_path, attr = self._lazy_subcommands[cmd_name]
        mod = importlib.import_module(mod_path)
        cmd = getattr(mod, attr)
        self.add_command(cmd, cmd_name)
        return cmd

    def format_commands(self, ctx, formatter):
        """Override to avoid importing lazy subcommands just for --help listing."""
        commands = []
        for subcommand in self.list_commands(ctx):
            if subcommand in self.commands:
                cmd = self.commands[subcommand]
                commands.append((subcommand, cmd.get_short_help_str(limit=150)))
            else:
                commands.append((subcommand, ""))

        if commands:
            with formatter.section("Commands"):
                formatter.write_dl(commands)


class NoHLClientCredentialsError(Exception):
    def __init__(self):
        message = (
            "\nNo way of determining credentials for HLClient "
            "could be found. \n"
            "\tOption 1: Use the --profile flag in the cli\n"
            "\tOption 2: Use the --api-token and --endpoint-url flags in the cli\n"
            "\tOption 3: export environment variables"
        )
        super().__init__(message)


class NoHLClient:
    """
    Fallback class for when a user does not provide a way
    of deterimining Highlighter credentials. This allows them
    to run help commands and other commands like 'config' that
    do not rely on a working HLClient, but will give a nice
    error message when they do.
    """

    def __getattr__(self, key):
        raise NoHLClientCredentialsError()

    def __repr__(self):
        return "NoHLClient"


def init_cli_context(
    ctx,
    api_token: Optional[str],
    endpoint_url: Optional[str],
    profile: Optional[str],
    profiles_path: Optional[str],
    highlighter_config_path: Optional[str],
):
    if ctx.obj is None:
        ctx.obj = {}

    logger = logging.getLogger(__name__)

    # Init HLClient — import deferred so that the CLI module can load without
    # pulling in the entire client dependency tree (boto3, numpy, etc.).
    from highlighter.client.gql_client import HLClient

    if profile is not None:
        client = HLClient.from_profile(profile=profile, profiles_path=profiles_path)
    elif (endpoint_url is not None) and (api_token is not None):
        client = HLClient.from_credential(
            api_token=api_token,
            endpoint_url=endpoint_url,
        )
    elif "HL_WEB_GRAPHQL_ENDPOINT" in os.environ:
        client = HLClient.get_client()
    else:
        client = NoHLClient()

    ctx.obj.update({"client": client})
    logger.debug(f"HLClient: {client}")

    # Load HighlighterRuntimeConfig
    try:
        hl_cfg = HighlighterRuntimeConfig.load(config_path=highlighter_config_path)
        ctx.obj.update({"hl_cfg": hl_cfg})
        logger.debug(f"Loaded HighlighterRuntimeConfig from: {hl_cfg.config_path}")
    except (HighlighterRuntimeConfigError, ValueError) as e:
        logger.error(f"Failed to load configuration: {e}")
        sys.exit(2)
    except Exception as e:
        logger.error(f"Unexpected error during loading configuration: {e}")
        sys.exit(1)


def _configure_windows_ssl():
    """Use the Windows certificate store for all SSL connections (corporate proxy support).

    truststore.inject_into_ssl() replaces ssl.SSLContext, but libraries that
    cached a reference to the *original* SSLContext before injection (botocore,
    urllib3) hit a RecursionError when their property setters resolve the class
    name dynamically via ssl.SSLContext.  We fix this by updating those cached
    references after injection — the same approach as truststore PR #180.
    """
    import truststore

    truststore.inject_into_ssl()

    # Patch cached SSLContext references that still point to the original class.
    # Without this, botocore's create_urllib3_context() and urllib3's
    # create_urllib3_context() create instances of the *old* ssl.SSLContext whose
    # property setters resolve the class name from the ssl module at call-time,
    # finding truststore's replacement and recursing infinitely.
    import importlib

    for module_path in ("botocore.httpsession", "urllib3.util.ssl_"):
        try:
            module = importlib.import_module(module_path)
            module.SSLContext = truststore.SSLContext
        except (ImportError, AttributeError):
            pass


@click.group("highlighter", cls=LazyGroup, lazy_subcommands=_LAZY_SUBCOMMANDS)
@click.option("--api-token", type=str)
@click.option("--endpoint-url", type=str)
@click.option("--profile", type=str, default=None)
@click.option("--profiles-path", type=str, default=_DEFAULT_PROFILES_YAML)
@click.option("--highlighter-config-path", type=str, default=None)
@click.pass_context
def highlighter_group(ctx, api_token, endpoint_url, profile, profiles_path, highlighter_config_path):
    if ctx.resilient_parsing:
        return  # Skip heavy init during --help / shell completion
    if sys.platform == "win32":
        _configure_windows_ssl()
    init_cli_context(ctx, api_token, endpoint_url, profile, profiles_path, highlighter_config_path)
    configure_root_logger(
        _log_path=ctx.obj["hl_cfg"].log_path,
        _log_level=ctx.obj["hl_cfg"].log_level,
        _log_rotation_max_kilobytes=ctx.obj["hl_cfg"].log_rotation_max_kilobytes,
        _log_rotation_backup_count=ctx.obj["hl_cfg"].log_rotation_backup_count,
    )
    warnings.simplefilter("default")  # Suppress duplicated warnings


@highlighter_group.command("write")
@click.argument("outfile", type=str)
@click.pass_context
def write(ctx, outfile):
    """Write credentials to a file.

    Each credential is appended to the given file.

    eg: highlighter-v2 --profile abc .envrc

    Results in the following lines being appended
    the .envrc

    export HL_WEB_GRAPHQL_ENDPOINT=...
    export HL_WEB_GRAPHQL_API_TOKEN=...
    """
    client = ctx.obj["client"]
    client.append_credentials_to_env_file(outfile)


@highlighter_group.command("export")
@click.pass_context
def export(ctx):
    """Export a profile's credentials to env

    Wrap this command in `back ticks` to export a profile's credentials to
    your environment

    eg: `highlighter-v2 --profile abc export`

    Results in the following credentials being
    added to your environment variables

    HL_WEB_GRAPHQL_ENDPOINT=...
    HL_WEB_GRAPHQL_API_TOKEN=...
    """
    client = ctx.obj["client"]

    click.echo("Wrap the command in `back ticks` to execute the exports", err=True)
    click.echo(
        f"export HL_WEB_GRAPHQL_ENDPOINT={client.endpoint_url} HL_WEB_GRAPHQL_API_TOKEN={client.api_token}"
    )


@highlighter_group.command("version")
def version():
    """Print the Highlighter SDK version."""
    click.echo(__version__)


if __name__ == "__main__":
    highlighter_group()
