"""Command-line interface for Azure discovery."""

from __future__ import annotations

import asyncio
import json
import sys
from pathlib import Path

import click
import typer
from click.core import ParameterSource
from pydantic import ValidationError

from .adt_types import (
    AzureDiscoveryRequest,
    AzureEnvironment,
)
from .orchestrator import run_discovery
from .utils.auth_validation import validate_auth
from .utils.config_files import deep_merge, load_config_file
from .utils.logging import enable_cli_logging

cli = typer.Typer(help="Azure tenant discovery and visualization CLI")


def _parse_tags(tag_args: list[str] | None) -> dict | None:
    if not tag_args:
        return None
    tags = {}
    for tag_arg in tag_args:
        if "=" not in tag_arg:
            raise typer.BadParameter("Tags must use key=value syntax")
        key, value = tag_arg.split("=", 1)
        tags[key.strip()] = value.strip()
    return tags


def _emit_cli_warnings(request: AzureDiscoveryRequest) -> None:
    if request.include_entra and request.graph_total_max_objects and request.graph_total_max_objects < 10:
        typer.echo(
            "Warning: --graph-total-max-objects is very small; later Graph collections may be skipped.",
            err=True,
        )

    if request.include_entra and not request.include_relationships:
        typer.echo(
            "Warning: --no-include-relationships disables Graph edges (has_member/has_owner/appId).",
            err=True,
        )


def _is_commandline(ctx: typer.Context, param_name: str) -> bool:
    source = ctx.get_parameter_source(param_name)
    return source == ParameterSource.COMMANDLINE


@cli.command("discover")
def discover_command(
    config: Path | None = typer.Option(
        None,
        "--config",
        help="Path to a JSON/TOML/YAML config file (AzureDiscoveryRequest shape). CLI flags override file values.",
    ),
    tenant_id: str | None = typer.Option(
        None,
        "--tenant-id",
        help="Entra ID tenant GUID (required unless provided in --config)",
    ),
    environment: AzureEnvironment = typer.Option(
        AzureEnvironment.AZURE_PUBLIC, help="Azure cloud environment"
    ),
    subscription: list[str] | None = typer.Option(
        None, "--subscription", "-s", help="Explicit subscription identifier"
    ),
    include_type: list[str] | None = typer.Option(
        None, "--include-type", help="Resource type to include (repeatable)"
    ),
    exclude_type: list[str] | None = typer.Option(
        None, "--exclude-type", help="Resource type to exclude (repeatable)"
    ),
    resource_group: list[str] | None = typer.Option(
        None, "--resource-group", help="Resource group filter (repeatable)"
    ),
    required_tag: list[str] | None = typer.Option(
        None, "--required-tag", help="Required tag key=value (repeatable)"
    ),
    include_entra: bool = typer.Option(
        False,
        "--include-entra",
        help="Include Entra ID resources via Microsoft Graph (requires Graph permissions)",
    ),
    # RBAC toggles
    include_rbac_assignments: bool = typer.Option(
        False,
        "--include-rbac-assignments",
        help="Include Azure role assignments in discovery",
    ),
    include_rbac_definitions: bool = typer.Option(
        False,
        "--include-rbac-definitions",
        help="Include Azure role definitions (built-in and custom)",
    ),
    rbac_scope: list[str] | None = typer.Option(
        None,
        "--rbac-scope",
        help="Filter role assignments by scope (repeatable)",
    ),
    # PIM toggles
    include_pim: bool = typer.Option(
        False,
        "--include-pim",
        help="Include PIM (Privileged Identity Management) eligible role assignments",
    ),
    pim_include_entra_eligibilities: bool = typer.Option(
        True,
        "--pim-include-entra-eligibilities/--no-pim-include-entra-eligibilities",
        help="Include Entra ID role eligibilities when PIM is enabled (default: true)",
    ),
    pim_include_entra_requests: bool = typer.Option(
        False,
        "--pim-include-entra-requests",
        help="Include pending/active Entra role eligibility requests",
    ),
    pim_include_azure_resource_eligibilities: bool = typer.Option(
        True,
        "--pim-include-azure-resource-eligibilities/--no-pim-include-azure-resource-eligibilities",
        help="Include Azure resource role eligibilities when PIM is enabled (default: true)",
    ),
    pim_scope: list[str] | None = typer.Option(
        None,
        "--pim-scope",
        help="Filter PIM eligibilities by scope (repeatable)",
    ),
    # Defender for Cloud toggles
    include_defender_cloud: bool = typer.Option(
        False,
        "--include-defender-cloud",
        help="Include Defender for Cloud security alerts, assessments, and scores",
    ),
    defender_include_alerts: bool = typer.Option(
        True,
        "--defender-include-alerts/--no-defender-include-alerts",
        help="Include security alerts from Defender for Cloud",
    ),
    defender_include_assessments: bool = typer.Option(
        True,
        "--defender-include-assessments/--no-defender-include-assessments",
        help="Include security assessments (vulnerability findings) from Defender for Cloud",
    ),
    defender_include_secure_scores: bool = typer.Option(
        True,
        "--defender-include-secure-scores/--no-defender-include-secure-scores",
        help="Include secure scores from Defender for Cloud",
    ),
    defender_alert_severity: list[str] | None = typer.Option(
        None,
        "--defender-alert-severity",
        help="Filter alerts by severity: High, Medium, Low, Informational (repeatable)",
    ),
    defender_alert_status: list[str] | None = typer.Option(
        None,
        "--defender-alert-status",
        help="Filter alerts by status: Active, Resolved, Dismissed (repeatable)",
    ),
    defender_assessment_severity: list[str] | None = typer.Option(
        None,
        "--defender-assessment-severity",
        help="Filter assessments by severity: High, Medium, Low (repeatable)",
    ),
    defender_assessment_status: list[str] | None = typer.Option(
        None,
        "--defender-assessment-status",
        help="Filter assessments by status: Healthy, Unhealthy, NotApplicable (repeatable)",
    ),
    # Scale controls
    scale_controls_enabled: bool = typer.Option(
        True,
        "--scale-controls/--no-scale-controls",
        help="Enable adaptive rate control for large tenants",
    ),
    scale_initial_rps: float = typer.Option(
        10.0,
        "--scale-initial-rps",
        help="Initial requests per second for adaptive rate control",
    ),
    scale_max_concurrent_batches: int = typer.Option(
        5,
        "--scale-max-concurrent-batches",
        help="Maximum concurrent batch operations",
    ),
    scale_initial_batch_size: int = typer.Option(
        1000,
        "--scale-initial-batch-size",
        help="Initial batch size for paginated operations",
    ),
    # Entra include toggles
    entra_include_organization: bool = typer.Option(
        True,
        "--entra-include-organization/--no-entra-include-organization",
        help="Include organization (tenant root)",
    ),
    entra_include_domains: bool = typer.Option(
        True,
        "--entra-include-domains/--no-entra-include-domains",
        help="Include tenant domains",
    ),
    entra_include_users: bool = typer.Option(
        True,
        "--entra-include-users/--no-entra-include-users",
        help="Include Entra users",
    ),
    entra_include_groups: bool = typer.Option(
        True,
        "--entra-include-groups/--no-entra-include-groups",
        help="Include Entra groups",
    ),
    entra_include_applications: bool = typer.Option(
        True,
        "--entra-include-applications/--no-entra-include-applications",
        help="Include Entra applications",
    ),
    entra_include_conditional_access_policies: bool = typer.Option(
        True,
        "--entra-include-conditional-access-policies/--no-entra-include-conditional-access-policies",
        help="Include conditional access policies (requires additional permissions)",
    ),
    entra_include_risky_users: bool = typer.Option(
        True,
        "--entra-include-risky-users/--no-entra-include-risky-users",
        help="Include risky users (requires additional permissions)",
    ),
    # Entra relationship caps
    entra_group_membership_max_groups: int = typer.Option(
        50,
        "--entra-group-membership-max-groups",
        help="Max groups to expand membership for (0 disables; requires --include-relationships)",
    ),
    entra_group_membership_max_members_per_group: int = typer.Option(
        200,
        "--entra-group-membership-max-members-per-group",
        help="Max members per group during expansion (0 disables; requires --include-relationships)",
    ),
    entra_ownership_max_apps: int = typer.Option(
        50,
        "--entra-ownership-max-apps",
        help="Max applications to expand owners for (0 disables; requires --include-relationships)",
    ),
    entra_ownership_max_owners_per_app: int = typer.Option(
        50,
        "--entra-ownership-max-owners-per-app",
        help="Max owners per app during expansion (0 disables; requires --include-relationships)",
    ),
    entra_sp_ownership_max_sps: int = typer.Option(
        50,
        "--entra-sp-ownership-max-sps",
        help="Max service principals to expand owners for (0 disables; requires --include-relationships)",
    ),
    entra_sp_ownership_max_owners_per_sp: int = typer.Option(
        50,
        "--entra-sp-ownership-max-owners-per-sp",
        help="Max owners per service principal during expansion (0 disables; requires --include-relationships)",
    ),
    validate_auth_flag: bool = typer.Option(
        False,
        "--validate-auth",
        help="Validate credentials/scopes and exit",
    ),
    probe_connectivity: bool = typer.Option(
        False,
        "--probe-connectivity",
        help="When validating auth, also run lightweight connectivity checks",
    ),
    include_metrics: bool = typer.Option(
        False,
        "--include-metrics/--no-include-metrics",
        help="Include merge and per-phase metrics in JSON output",
    ),
    materialize_missing_endpoints: bool = typer.Option(
        False,
        "--materialize-missing-endpoints/--no-materialize-missing-endpoints",
        help="Create stub nodes for missing relationship endpoints so edges are preserved",
    ),
    include_relationships: bool = typer.Option(
        True,
        "--include-relationships/--no-include-relationships",
        help="Include inferred and expanded relationships/edges",
    ),
    graph_total_max_objects: int = typer.Option(
        0,
        "--graph-total-max-objects",
        help="Maximum total objects across all Graph collections (0 = unlimited)",
    ),
    entra_max_objects: int = typer.Option(
        0,
        "--entra-max-objects",
        help="Maximum objects per Entra collection (0 = unlimited)",
    ),
    throttle_delay_seconds: float = typer.Option(
        0.05,
        "--throttle-delay-seconds",
        help="Delay between Graph page fetches (seconds)",
    ),
    prefer_cli: bool = typer.Option(
        False, "--prefer-cli", help="Prefer Azure CLI credential in chain"
    ),
    visualization_output_dir: Path = typer.Option(
        Path("artifacts/graphs"),
        "--visualization-output-dir",
        help="Directory for HTML output",
    ),
    visualization_file: str | None = typer.Option(
        None, "--visualization-file", help="Optional HTML file name"
    ),
    preview_request: bool = typer.Option(
        False,
        "--preview-request/--no-preview-request",
        "--dry-run/--no-dry-run",
        help="Print the constructed discovery request JSON and exit",
    ),
    output: Path | None = typer.Option(
        None, "--output", "-o", help="Write JSON output to file instead of stdout"
    ),
    quiet: bool = typer.Option(
        False, "--quiet", "-q", help="Suppress all logs except errors"
    ),
    format: str = typer.Option(
        "json", "--format", "-f", help="Output format: json, json-compact"
    ),
) -> None:
    """Discover Azure resources and emit JSON output."""

    enable_cli_logging(quiet=quiet)

    ctx = click.get_current_context()

    base: dict = {}
    if config:
        base = load_config_file(config)

    overrides: dict = {}

    if tenant_id is not None and _is_commandline(ctx, "tenant_id"):
        overrides["tenant_id"] = tenant_id

    if _is_commandline(ctx, "environment"):
        overrides["environment"] = environment

    if _is_commandline(ctx, "subscription"):
        overrides["subscriptions"] = subscription

    if _is_commandline(ctx, "include_entra"):
        overrides["include_entra"] = include_entra

    # RBAC overrides
    if _is_commandline(ctx, "include_rbac_assignments"):
        overrides["include_rbac_assignments"] = include_rbac_assignments
    if _is_commandline(ctx, "include_rbac_definitions"):
        overrides["include_rbac_definitions"] = include_rbac_definitions
    if _is_commandline(ctx, "rbac_scope"):
        overrides["rbac_scope_filter"] = rbac_scope

    # PIM overrides
    if _is_commandline(ctx, "include_pim"):
        overrides["include_pim"] = include_pim
    if _is_commandline(ctx, "pim_scope"):
        overrides["pim_scope_filter"] = pim_scope

    pim_overrides: dict = {}
    if _is_commandline(ctx, "pim_include_entra_eligibilities"):
        pim_overrides["include_entra_role_eligibilities"] = pim_include_entra_eligibilities
    if _is_commandline(ctx, "pim_include_entra_requests"):
        pim_overrides["include_entra_role_eligibility_requests"] = pim_include_entra_requests
    if _is_commandline(ctx, "pim_include_azure_resource_eligibilities"):
        pim_overrides["include_azure_resource_eligibilities"] = pim_include_azure_resource_eligibilities

    if pim_overrides:
        overrides["pim_config"] = pim_overrides

    # Defender for Cloud overrides
    if _is_commandline(ctx, "include_defender_cloud"):
        overrides["include_defender_cloud"] = include_defender_cloud

    defender_overrides: dict = {}
    if _is_commandline(ctx, "defender_include_alerts"):
        defender_overrides["include_security_alerts"] = defender_include_alerts
    if _is_commandline(ctx, "defender_include_assessments"):
        defender_overrides["include_security_assessments"] = defender_include_assessments
    if _is_commandline(ctx, "defender_include_secure_scores"):
        defender_overrides["include_secure_scores"] = defender_include_secure_scores
    if _is_commandline(ctx, "defender_alert_severity"):
        defender_overrides["alert_severity_filter"] = defender_alert_severity
    if _is_commandline(ctx, "defender_alert_status"):
        defender_overrides["alert_status_filter"] = defender_alert_status
    if _is_commandline(ctx, "defender_assessment_severity"):
        defender_overrides["assessment_severity_filter"] = defender_assessment_severity
    if _is_commandline(ctx, "defender_assessment_status"):
        defender_overrides["assessment_status_filter"] = defender_assessment_status
    if defender_overrides:
        overrides["defender_config"] = defender_overrides

    # Scale control overrides (nested)
    scale_overrides: dict = {}
    if _is_commandline(ctx, "scale_controls_enabled"):
        scale_overrides["enabled"] = scale_controls_enabled
    if _is_commandline(ctx, "scale_initial_rps"):
        scale_overrides["initial_rps"] = scale_initial_rps
    if _is_commandline(ctx, "scale_max_concurrent_batches"):
        scale_overrides["max_concurrent_batches"] = scale_max_concurrent_batches
    if _is_commandline(ctx, "scale_initial_batch_size"):
        scale_overrides["initial_batch_size"] = scale_initial_batch_size
    if scale_overrides:
        overrides["scale_controls"] = scale_overrides

    if _is_commandline(ctx, "include_metrics"):
        overrides["include_metrics"] = include_metrics

    if _is_commandline(ctx, "materialize_missing_endpoints"):
        overrides["materialize_missing_endpoints"] = materialize_missing_endpoints

    if _is_commandline(ctx, "include_relationships"):
        overrides["include_relationships"] = include_relationships

    if _is_commandline(ctx, "graph_total_max_objects"):
        overrides["graph_total_max_objects"] = graph_total_max_objects

    if _is_commandline(ctx, "entra_max_objects"):
        overrides["entra_max_objects"] = entra_max_objects

    if _is_commandline(ctx, "throttle_delay_seconds"):
        overrides["throttle_delay_seconds"] = throttle_delay_seconds

    if _is_commandline(ctx, "prefer_cli"):
        overrides["prefer_cli_credentials"] = prefer_cli

    # Entra toggles
    for name in (
        "entra_include_organization",
        "entra_include_domains",
        "entra_include_users",
        "entra_include_groups",
        "entra_include_applications",
        "entra_include_conditional_access_policies",
        "entra_include_risky_users",
        "entra_group_membership_max_groups",
        "entra_group_membership_max_members_per_group",
        "entra_ownership_max_apps",
        "entra_ownership_max_owners_per_app",
        "entra_sp_ownership_max_sps",
        "entra_sp_ownership_max_owners_per_sp",
    ):
        if _is_commandline(ctx, name):
            overrides[name] = locals()[name]

    # Filter overrides (nested)
    filter_overrides: dict = {}
    if _is_commandline(ctx, "include_type"):
        filter_overrides["include_types"] = include_type
    if _is_commandline(ctx, "exclude_type"):
        filter_overrides["exclude_types"] = exclude_type
    if _is_commandline(ctx, "resource_group"):
        filter_overrides["resource_groups"] = resource_group
    if _is_commandline(ctx, "required_tag"):
        filter_overrides["required_tags"] = _parse_tags(required_tag)

    if filter_overrides:
        overrides["filter"] = filter_overrides

    # Visualization overrides (nested)
    visualization_overrides: dict = {}
    if _is_commandline(ctx, "visualization_output_dir"):
        visualization_overrides["output_dir"] = visualization_output_dir
    if _is_commandline(ctx, "visualization_file"):
        visualization_overrides["file_name"] = visualization_file
    if visualization_overrides:
        overrides["visualization"] = visualization_overrides

    merged = deep_merge(base, overrides)

    if not merged.get("tenant_id"):
        raise typer.BadParameter("tenant_id is required (provide --tenant-id or set tenant_id in --config)")

    try:
        request = AzureDiscoveryRequest.model_validate(merged)
    except ValidationError as exc:
        raise typer.BadParameter(str(exc)) from exc

    _emit_cli_warnings(request)

    if preview_request:
        sys.stdout.write(json.dumps(request.model_dump(), indent=2, default=str) + "\n")
        return

    if validate_auth_flag:
        sys.stdout.write(
            json.dumps(validate_auth(request, probe_connectivity=probe_connectivity), indent=2)
            + "\n"
        )
        return

    response = asyncio.run(run_discovery(request))

    if format == "json-compact":
        json_output = json.dumps(response.model_dump(), default=str)
    else:
        json_output = json.dumps(response.model_dump(), indent=2, default=str)

    if output:
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(json_output)
        typer.echo(f"Discovery results written to {output}", err=True)
    else:
        sys.stdout.write(json_output + "\n")


def main() -> None:
    cli()


if __name__ == "__main__":
    main()
