import json
import logging
import os
import shutil
from pathlib import Path
from typing import Any

import rich_click as click
import yaml

from snowcap import LOGO
from snowcap.blueprint import dump_plan
from snowcap.enums import BlueprintScope
from snowcap.gitops import (
    collect_configs_from_path,
    collect_vars_from_environment,
    merge_configs,
    merge_vars,
    parse_resources,
)
from snowcap.operations.blueprint import (
    blueprint_apply,
    blueprint_apply_plan,
    blueprint_plan,
)
from snowcap.operations.connector import connect, get_env_vars
from snowcap.operations.export import export_resources

click.rich_click.ERRORS_SUGGESTION = "Try using the '--debug' flag for more information.\n"


class ScopeParamType(click.ParamType):
    name = "scope"

    def convert(self, value, param, ctx):
        return BlueprintScope(value)


class JsonParamType(click.ParamType):
    name = "json"

    def convert(self, value, param, ctx):
        try:
            return json.loads(value)
        except json.JSONDecodeError:
            self.fail(f"'{value}' is not a valid JSON string", param, ctx)


class CommaSeparatedListParamType(click.ParamType):
    name = "comma_separated_list"

    def convert(self, value, param, ctx):
        return parse_resources(value)


def load_plan(plan_file):
    with open(plan_file, "r") as f:
        plan = json.load(f)
    return plan


def get_version():
    """Get version from package metadata."""
    try:
        from importlib.metadata import version

        return version("snowcap")
    except Exception:
        return "unknown"


def version_callback(ctx, param, value):
    """Custom version callback that shows logo and version."""
    if not value or ctx.resilient_parsing:
        return
    click.echo(LOGO)
    click.echo(f"snowcap version {get_version()}")
    ctx.exit()


@click.group()
@click.option(
    "--version",
    is_flag=True,
    callback=version_callback,
    expose_value=False,
    is_eager=True,
    help="Show version and exit.",
)
def snowcap_cli():
    """snowcap helps you manage your Snowflake environment."""
    pass


# Shared click options


def config_path_option():
    return click.option(
        "--config",
        "config_path",
        type=str,
        help="Path to configuration YAML file or directory",
        metavar="<file_or_dir>",
    )


def vars_option():
    return click.option(
        "--vars",
        type=JsonParamType(),
        help="Dynamic values, specified as a JSON dictionary",
        metavar="<JSON_string>",
    )


def sync_resources_option():
    return click.option(
        "--sync_resources",
        type=CommaSeparatedListParamType(),
        help="List of resources types to sync in the plan. If not specified, all resources are created or updated, never deleted.",
        metavar="<resource_types>",
    )


def exclude_resources_option():
    return click.option(
        "--exclude",
        "exclude_resources",
        type=CommaSeparatedListParamType(),
        help="Exclude resource types from the plan (e.g., --exclude masking_policy)",
        metavar="<resource_types>",
    )


def scope_option():
    return click.option(
        "--scope",
        type=ScopeParamType(),
        help="Limit the scope of resources to a specific database or schema",
        metavar="<scope>",
    )


def database_option():
    return click.option(
        "--database",
        type=str,
        help="Database to limit the scope to",
        metavar="<database_name>",
    )


def schema_option():
    return click.option(
        "--schema",
        type=str,
        help="Schema to limit the scope to",
        metavar="<schema_name>",
    )


def debug_option():
    return click.option(
        "--debug",
        is_flag=True,
        default=False,
        help="Enable debug mode, which will print additional information to the console",
    )


def use_account_usage_option():
    return click.option(
        "--use-account-usage",
        "use_account_usage",
        is_flag=True,
        default=False,
        help="Use ACCOUNT_USAGE views for bulk grant fetching (faster for 50+ roles)",
    )


@snowcap_cli.command("plan", no_args_is_help=True)
@config_path_option()
@click.option("--json", "json_output", is_flag=True, help="Output plan in machine-readable JSON format")
@click.option("--out", "output_file", type=str, help="Write plan to a file", metavar="<filename>")
@vars_option()
@sync_resources_option()
@exclude_resources_option()
@scope_option()
@database_option()
@schema_option()
@use_account_usage_option()
@debug_option()
def plan(
    config_path,
    json_output,
    output_file,
    vars: dict,
    sync_resources,
    exclude_resources,
    scope,
    database,
    schema,
    use_account_usage,
    debug,
):
    """Compare a resource config to the current state of Snowflake"""

    if debug:
        logging.basicConfig(level=logging.DEBUG, format="%(levelname)s - %(name)s - %(message)s")

    if not config_path:
        raise click.UsageError("--config is required")

    yaml_config: dict[str, Any] = {}
    configs = collect_configs_from_path(config_path)
    for config in configs:
        yaml_config = merge_configs(yaml_config, config[1])

    cli_config: dict[str, Any] = {}
    if vars:
        cli_config["vars"] = vars
    if sync_resources:
        cli_config["sync_resources"] = sync_resources
    if exclude_resources:
        cli_config["exclude_resources"] = exclude_resources
    if scope:
        cli_config["scope"] = scope
    if database:
        cli_config["database"] = database
    if schema:
        cli_config["schema"] = schema
    if use_account_usage:
        cli_config["use_account_usage"] = use_account_usage

    env_vars = collect_vars_from_environment()
    if env_vars:
        cli_config["vars"] = merge_vars(cli_config.get("vars", {}), env_vars)

    try:
        plan_obj = blueprint_plan(yaml_config, cli_config)
        if output_file:
            with open(output_file, "w") as f:
                f.write(dump_plan(plan_obj, format="json"))
        else:
            output = None
            if json_output:
                output = dump_plan(plan_obj, format="json")
            else:
                output = dump_plan(plan_obj, format="text")
            print(output)
    except Exception as e:
        if debug:
            raise e
        else:
            raise click.UsageError(str(e))


@snowcap_cli.command("apply", no_args_is_help=True)
@config_path_option()
@click.option("--plan", "plan_file", type=str, help="Path to plan JSON file", metavar="<filename>")
@vars_option()
@sync_resources_option()
@exclude_resources_option()
@scope_option()
@database_option()
@schema_option()
@click.option("--dry-run", is_flag=True, help="When dry run is true, Snowcap will not make any changes to Snowflake")
@use_account_usage_option()
@click.option("--verbose", "-v", is_flag=True, help="Show SQL commands being executed")
@debug_option()
def apply(
    config_path,
    plan_file,
    vars,
    sync_resources,
    exclude_resources,
    scope,
    database,
    schema,
    dry_run,
    use_account_usage,
    verbose,
    debug,
) -> None:
    """Apply a resource config to a Snowflake account"""

    if debug:
        logging.basicConfig(level=logging.DEBUG, format="%(levelname)s - %(name)s - %(message)s")
    elif verbose:
        logging.basicConfig(level=logging.DEBUG, format="%(message)s")

    if config_path and plan_file:
        raise click.UsageError("Cannot specify both --config and --plan.")
    if not config_path and not plan_file:
        raise click.UsageError("Either --config or --plan must be specified.")

    cli_config: dict[str, Any] = {}
    if vars:
        cli_config["vars"] = vars
    if dry_run:
        cli_config["dry_run"] = dry_run
    if sync_resources:
        cli_config["sync_resources"] = sync_resources
    if exclude_resources:
        cli_config["exclude_resources"] = exclude_resources
    if scope:
        cli_config["scope"] = scope
    if database:
        cli_config["database"] = database
    if schema:
        cli_config["schema"] = schema
    if use_account_usage:
        cli_config["use_account_usage"] = use_account_usage

    try:
        env_vars = collect_vars_from_environment()
        if env_vars:
            cli_config["vars"] = merge_vars(cli_config.get("vars", {}), env_vars)

        if config_path:
            yaml_config: dict[str, Any] = {}
            configs = collect_configs_from_path(config_path)
            for config in configs:
                yaml_config = merge_configs(yaml_config, config[1])
            blueprint_apply(yaml_config, cli_config)
        elif plan_file:
            plan_obj = load_plan(plan_file)
            blueprint_apply_plan(plan_obj, cli_config)
        else:
            raise click.UsageError("No config or plan file specified")
    except Exception as e:
        if debug:
            raise e
        else:
            raise click.UsageError(str(e))


@snowcap_cli.command("export", context_settings={"show_default": True}, no_args_is_help=True)
@click.option(
    "--resource",
    "resources",
    type=CommaSeparatedListParamType(),
    help="The resource types to export",
    metavar="<resource_types>",
)
@click.option("--all", "export_all", is_flag=True, help="Export all resources")
@click.option(
    "--exclude",
    "exclude_resources",
    type=CommaSeparatedListParamType(),
    help="Exclude resources, used with --all",
    metavar="<resource_types>",
)
@click.option("--out", type=str, help="Write exported config to a file", metavar="<filename>")
@click.option("--format", type=click.Choice(["json", "yml"]), default="yml", help="Output format")
def export(resources, export_all, exclude_resources, out, format) -> None:
    """
    Generate a resource config for existing Snowflake resources

    This command allows you to export resources from Snowflake in either JSON or YAML format.
    You can specify the type of resource to export and the output filename for the exported data.

    Resource types are specified with snake case (eg. Warehouse => warehouse, NetworkRule => network_rule, etc.).
    You can also export all resources by using the --all flag. Specify multiple resource types by separating them with commas.

    Examples:

    \b
    # Export all database configurations to a file
    snowcap export --resource=database --out=databases.yml --format=yml

    \b
    # Export all resources
    snowcap export --all --out=snowcap.yml --format=yml

    \b
    # Export all resources except for users and roles
    snowcap export --all --exclude=user,role --out=snowcap.yml
    """

    if resources and export_all:
        raise click.UsageError("You can't specify both --resource and --all options at the same time.")

    resource_config: dict[str, Any] = {}
    if resources:
        resource_config = export_resources(include=resources)
    elif export_all:
        resource_config = export_resources(exclude=exclude_resources)
    else:
        raise

    output = None
    if format == "json":
        output = json.dumps(resource_config, indent=2)
    elif format == "yml":
        output = yaml.dump(resource_config, sort_keys=False)
    else:
        raise ValueError(f"Unsupported format: `{format}`")

    if out:
        with open(out, "w") as f:
            f.write(output)
    else:
        print(output)


@snowcap_cli.command("connect")
def cli_connect():
    """Test the connection to Snowflake"""
    env_vars = get_env_vars()
    if not env_vars:
        raise click.UsageError("No environment variables found. Please set the environment variables and try again.")
    for key, value in env_vars.items():
        value_inspect = "********" if key in ["password", "mfa_passcode"] else value
        print(f"SNOWFLAKE_{key.upper()}={value_inspect}")
    session = connect()
    print(f"Connection successful as user {session.user}")


@snowcap_cli.group()
def generate():
    """Generate helper files for integrations."""
    pass


def get_dbt_path_default():
    """Get the default dbt project path from environment variables."""
    return os.environ.get("DATACOVES__DBT_HOME") or os.environ.get("DBT_HOME") or ""


@generate.command("dbt-macros")
@click.option(
    "--dbt-path",
    prompt="dbt project path",
    default=get_dbt_path_default,
    help="Path to dbt project (checks DATACOVES__DBT_HOME and DBT_HOME env vars)",
)
@click.option("--macros-path", default="macros", help="Macros directory name relative to dbt project")
@click.option(
    "--tag-database",
    prompt="Tag database",
    default="GOVERNANCE",
    help="Database where tags are defined",
)
@click.option(
    "--tag-schema",
    prompt="Tag schema",
    default="TAGS",
    help="Schema where tags are defined",
)
@click.option(
    "--policy-database",
    prompt="Policy database",
    default="GOVERNANCE",
    help="Database where row access policies are defined",
)
@click.option(
    "--policy-schema",
    prompt="Policy schema",
    default="POLICIES",
    help="Schema where row access policies are defined",
)
def dbt_macros(dbt_path, macros_path, tag_database, tag_schema, policy_database, policy_schema):
    """Generate dbt macros for applying Snowflake governance policies."""
    dbt_project_path = Path(dbt_path)

    if not dbt_project_path.exists():
        raise click.UsageError(f"dbt project path does not exist: {dbt_project_path}")

    dbt_project_file = dbt_project_path / "dbt_project.yml"
    if not dbt_project_file.exists():
        raise click.UsageError(f"No dbt_project.yml found in: {dbt_project_path}")

    macros_dir = dbt_project_path / macros_path
    macros_dir.mkdir(parents=True, exist_ok=True)

    template_path = Path(__file__).parent / "templates" / "snowcap_apply_tags.sql"
    dest_path = macros_dir / "snowcap_apply_tags.sql"

    shutil.copy(template_path, dest_path)

    click.echo(f"\nCreated {dest_path}\n")
    click.echo("Add the following to your dbt_project.yml:\n")
    click.echo("vars:")
    click.echo("  # For column masking tags")
    click.echo(f"  snowcap_tag_database: {tag_database}")
    click.echo(f"  snowcap_tag_schema: {tag_schema}")
    click.echo("")
    click.echo("  # For row access policies")
    click.echo(f"  snowcap_policy_database: {policy_database}")
    click.echo(f"  snowcap_policy_schema: {policy_schema}")
    click.echo("")
    click.echo("models:")
    click.echo("  <your_project>:")
    click.echo("    +post-hook:")
    click.echo('      - "{{ snowcap_apply_policies() }}"')
    click.echo("")
    click.echo("Then use meta in your schema.yml:\n")
    click.echo("models:")
    click.echo("  - name: orders")
    click.echo("    meta:")
    click.echo("      row_access_policy: rap_country    # Row-level filtering")
    click.echo("      row_access_column: country_code")
    click.echo("    columns:")
    click.echo("      - name: email")
    click.echo("        meta:")
    click.echo("          masking_tag: pii              # Column masking")


if __name__ == "__main__":
    snowcap_cli()
