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

from __future__ import annotations

import asyncio
import json
import logging
import subprocess
import sys
from pathlib import Path
from typing import Any, NamedTuple

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

from .adt_types import (
    AzureClientError,
    AzureDiscoveryRequest,
    AzureDiscoveryResponse,
    AzureEnvironment,
)
from .enumerators import list_policy_initiatives
from .orchestrator import run_discovery
from .utils import build_environment_config, ensure_subscription_ids, get_credential
from .utils.auth_validation import validate_auth
from .utils.config_files import deep_merge, load_config_file, write_config_file
from .utils.logging import enable_cli_logging

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


class PanelLogHandler(logging.Handler):
    """Redirects log records to a transient Rich panel."""

    def __init__(self, panel: Any) -> None:
        super().__init__()
        self.panel = panel

    def emit(self, record: logging.LogRecord) -> None:
        try:
            msg = self.format(record)
            self.panel.append(msg)
        except Exception:
            self.handleError(record)


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 _is_credential_failure(exc: BaseException) -> bool:
    """True if the exception chain looks like an Azure credential/auth failure."""
    msg = str(exc).lower()
    for _ in range(5):  # follow cause chain
        if not msg:
            break
        if (
            "chainedtokencredential failed" in msg
            or "defaultazurecredential failed" in msg
            or "azureclicredential: failed to invoke" in msg
            or "credential failed to retrieve a token" in msg
        ):
            return True
        exc = getattr(exc, "__cause__", None) or getattr(exc, "__context__", None)
        if exc is None:
            break
        msg = str(exc).lower()
    return False


def _emit_auth_failure_hint(
    request: AzureDiscoveryRequest,
    exc: BaseException,
) -> None:
    """Print a short, actionable hint when discovery fails due to auth."""
    if not _is_credential_failure(exc):
        return
    typer.echo("", err=True)
    typer.echo("Authentication failed. To fix:", err=True)
    typer.echo("  1. Run:  az login", err=True)
    if request.environment == AzureEnvironment.AZURE_GOV:
        typer.echo(
            "  2. For Azure Government, run:  az cloud set --name AzureUSGovernment",
            err=True,
        )
        typer.echo("     then run  az login  again.", err=True)
    typer.echo(
        "  3. Optionally set  prefer_cli_credentials: true  in your config.",
        err=True,
    )
    typer.echo("", err=True)


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,
        )

    if request.include_compliance and not request.compliance_include_policy_assignments:
        typer.echo(
            "Warning: --no-compliance-include-policy-assignments disables compliance mapping.",
            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("version", help="Show package version and exit.")
def version_command() -> None:
    from . import __version__
    typer.echo(__version__)


# Map Azure CLI environmentName to our AzureEnvironment enum
_AZ_ENV_TO_ENUM: dict[str, AzureEnvironment] = {
    "AzureCloud": AzureEnvironment.AZURE_PUBLIC,
    "AzureUSGovernment": AzureEnvironment.AZURE_GOV,
    "AzureChinaCloud": AzureEnvironment.AZURE_CHINA,
    "AzureGermanCloud": AzureEnvironment.AZURE_GERMANY,
    "AzureStack": AzureEnvironment.AZURE_STACK,
}


class _AzCliContext(NamedTuple):
    """Current Azure CLI account context."""

    tenant_id: str
    subscription_id: str | None
    subscription_name: str | None
    all_subscription_ids: list[str]
    environment: AzureEnvironment | None  # from environmentName when present


def _get_az_cli_context() -> tuple[_AzCliContext | None, str]:
    """Return (current tenant/subscription from Azure CLI, or None; stderr from az if failed)."""
    stderr_ref: list[str] = []
    try:
        result = subprocess.run(
            ["az", "account", "show", "--output", "json"],
            capture_output=True,
            text=True,
            timeout=10,
            check=False,
        )
        if result.returncode != 0:
            if result.stderr and result.stderr.strip():
                stderr_ref.append(result.stderr.strip())
            return None, "\n".join(stderr_ref)
        if not result.stdout.strip():
            return None, "\n".join(stderr_ref)
        account = json.loads(result.stdout)
        tenant_id = account.get("tenantId") or account.get("tenant_id")
        subscription_id = account.get("id") or account.get("subscriptionId")
        subscription_name = account.get("name") or account.get("user", {}).get("name")
        if not tenant_id:
            return None, "\n".join(stderr_ref)

        env_name = account.get("environmentName") or account.get("environment_name") or ""
        environment = _AZ_ENV_TO_ENUM.get(env_name) if env_name else None

        all_ids: list[str] = []
        list_result = subprocess.run(
            ["az", "account", "list", "--output", "json", "--query", "[].id"],
            capture_output=True,
            text=True,
            timeout=10,
            check=False,
        )
        if list_result.returncode == 0 and list_result.stdout.strip():
            try:
                all_ids = json.loads(list_result.stdout)
            except json.JSONDecodeError:
                pass
        return (
            _AzCliContext(
                tenant_id=tenant_id,
                subscription_id=subscription_id,
                subscription_name=subscription_name,
                all_subscription_ids=all_ids,
                environment=environment,
            ),
            "",
        )
    except FileNotFoundError:
        return None, "az not found (Azure CLI not installed or not on PATH)."
    except subprocess.TimeoutExpired:
        return None, "az account show timed out."
    except (json.JSONDecodeError, KeyError) as e:
        return None, str(e)


@cli.command("list-initiatives")
def list_initiatives_command(
    tenant_id: str = typer.Option(
        ..., "--tenant-id", help="Entra ID tenant GUID"
    ),
    environment: AzureEnvironment = typer.Option(
        AzureEnvironment.AZURE_PUBLIC, help="Azure cloud environment"
    ),
    subscription: list[str] | None = typer.Option(
        None, "--subscription", "-s", help="Explicit subscription identifier (repeatable)"
    ),
    prefer_cli: bool = typer.Option(
        False, "--prefer-cli", help="Prefer Azure CLI credential in chain"
    ),
    name_contains: str | None = typer.Option(
        None, "--filter", help="Filter initiatives whose display name contains this substring"
    ),
    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:
    """List Azure Policy initiatives available in the tenant."""

    enable_cli_logging(quiet=quiet)

    request = AzureDiscoveryRequest(
        tenant_id=tenant_id,
        environment=environment,
        subscriptions=subscription,
        prefer_cli_credentials=prefer_cli,
    )

    env_config = build_environment_config(request.environment)
    credential = get_credential(request, env_config)

    subscriptions_resolved = asyncio.run(
        ensure_subscription_ids(request, credential, base_url=env_config.resource_manager)
    )

    initiatives = asyncio.run(
        list_policy_initiatives(
            credential=credential,
            subscriptions=subscriptions_resolved,
            base_url=env_config.resource_manager,
            batch_size=request.max_batch_size,
            name_contains=name_contains,
        )
    )

    payload = [i.model_dump() for i in initiatives]

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

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


@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_azure_resources: bool = typer.Option(
        True,
        "--include-azure-resources/--no-include-azure-resources",
        help="Include Azure resources from subscriptions (default: true)",
    ),
    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)",
    ),
    # Microsoft 365 toggles
    include_m365: bool = typer.Option(
        False,
        "--include-m365",
        help="Include Microsoft 365 resources (SharePoint, Teams, OneDrive, Exchange)",
    ),
    m365_include_sharepoint: bool = typer.Option(
        True,
        "--m365-sharepoint/--no-m365-sharepoint",
        help="Include SharePoint sites in M365 enumeration",
    ),
    m365_include_teams: bool = typer.Option(
        True,
        "--m365-teams/--no-m365-teams",
        help="Include Microsoft Teams in M365 enumeration",
    ),
    m365_include_onedrive: bool = typer.Option(
        True,
        "--m365-onedrive/--no-m365-onedrive",
        help="Include OneDrive drives in M365 enumeration",
    ),
    m365_include_exchange: bool = typer.Option(
        False,
        "--m365-exchange/--no-m365-exchange",
        help="Include Exchange mailboxes in M365 enumeration (requires Mail.Read.All)",
    ),
    m365_site_filter: list[str] | None = typer.Option(
        None,
        "--m365-site-filter",
        help="Filter SharePoint sites by URL pattern (repeatable)",
    ),
    m365_user_filter: list[str] | None = typer.Option(
        None,
        "--m365-user-filter",
        help="Filter OneDrive/Exchange by user UPN (repeatable)",
    ),
    m365_max_sites: int = typer.Option(
        1000,
        "--m365-max-sites",
        help="Maximum SharePoint sites to enumerate",
    ),
    m365_max_teams: int = typer.Option(
        1000,
        "--m365-max-teams",
        help="Maximum Teams to enumerate",
    ),
    m365_max_drives: int = typer.Option(
        1000,
        "--m365-max-drives",
        help="Maximum OneDrive drives to enumerate",
    ),
    m365_max_mailboxes: int = typer.Option(
        1000,
        "--m365-max-mailboxes",
        help="Maximum Exchange mailboxes to enumerate",
    ),
    m365_filter_archived_teams: bool = typer.Option(
        True,
        "--m365-filter-archived-teams/--no-m365-filter-archived-teams",
        help="Exclude archived Teams from enumeration",
    ),
    # Microsoft Purview toggles
    include_purview: bool = typer.Option(
        False,
        "--include-purview",
        help="Include Microsoft Purview data governance and compliance resources",
    ),
    purview_include_sensitivity_labels: bool = typer.Option(
        True,
        "--purview-sensitivity-labels/--no-purview-sensitivity-labels",
        help="Include sensitivity labels for information protection",
    ),
    purview_include_dlp_policies: bool = typer.Option(
        True,
        "--purview-dlp-policies/--no-purview-dlp-policies",
        help="Include Data Loss Prevention (DLP) policies",
    ),
    purview_include_retention_policies: bool = typer.Option(
        True,
        "--purview-retention-policies/--no-purview-retention-policies",
        help="Include retention and deletion policies",
    ),
    purview_include_retention_labels: bool = typer.Option(
        True,
        "--purview-retention-labels/--no-purview-retention-labels",
        help="Include retention labels for records management",
    ),
    purview_include_information_protection_policies: bool = typer.Option(
        True,
        "--purview-information-protection-policies/--no-purview-information-protection-policies",
        help="Include information protection policies",
    ),
    purview_label_filter: list[str] | None = typer.Option(
        None,
        "--purview-label-filter",
        help="Filter sensitivity/retention labels by name pattern (repeatable)",
    ),
    purview_policy_filter: list[str] | None = typer.Option(
        None,
        "--purview-policy-filter",
        help="Filter DLP/retention policies by name pattern (repeatable)",
    ),
    purview_max_labels: int = typer.Option(
        1000,
        "--purview-max-labels",
        help="Maximum sensitivity/retention labels to enumerate",
    ),
    purview_max_dlp_policies: int = typer.Option(
        1000,
        "--purview-max-dlp-policies",
        help="Maximum DLP policies to enumerate",
    ),
    purview_max_retention_policies: int = typer.Option(
        1000,
        "--purview-max-retention-policies",
        help="Maximum retention policies to enumerate",
    ),
    purview_include_disabled: bool = typer.Option(
        False,
        "--purview-include-disabled/--no-purview-include-disabled",
        help="Include disabled DLP and retention policies",
    ),
    # 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 Enra 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)",
    ),
    # Compliance mapping flags
    include_compliance: bool = typer.Option(
        False,
        "--include-compliance",
        help="Map resources to compliance controls via Azure Policy assignments (NIST, CMMC, ISO, etc.)",
    ),
    compliance_include_policy_assignments: bool = typer.Option(
        True,
        "--compliance-include-policy-assignments/--no-compliance-include-policy-assignments",
        help="Include policy assignment mappings for compliance controls",
    ),
    compliance_include_policy_states: bool = typer.Option(
        True,
        "--compliance-include-policy-states/--no-compliance-include-policy-states",
        help="Include policy compliance states (Compliant/NonCompliant)",
    ),
    compliance_max_assignments_per_scope: int = typer.Option(
        100,
        "--compliance-max-assignments-per-scope",
        help="Max policy assignments to map per resource scope (0 = unlimited)",
    ),
    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_azure_resources"):
        overrides["include_azure_resources"] = include_azure_resources

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

    if _is_commandline(ctx, "include_compliance"):
        overrides["include_compliance"] = include_compliance
    # 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

    # M365 overrides
    if _is_commandline(ctx, "include_m365"):
        overrides["include_m365"] = include_m365

    m365_overrides: dict = {}
    if _is_commandline(ctx, "m365_include_sharepoint"):
        m365_overrides["include_sharepoint_sites"] = m365_include_sharepoint
    if _is_commandline(ctx, "m365_include_teams"):
        m365_overrides["include_teams"] = m365_include_teams
    if _is_commandline(ctx, "m365_include_onedrive"):
        m365_overrides["include_onedrive"] = m365_include_onedrive
    if _is_commandline(ctx, "m365_include_exchange"):
        m365_overrides["include_exchange_mailboxes"] = m365_include_exchange
    if _is_commandline(ctx, "m365_site_filter"):
        m365_overrides["sharepoint_site_filter"] = m365_site_filter
    if _is_commandline(ctx, "m365_user_filter"):
        m365_overrides["onedrive_user_filter"] = m365_user_filter
        m365_overrides["exchange_user_filter"] = m365_user_filter
    if _is_commandline(ctx, "m365_max_sites"):
        m365_overrides["sharepoint_max_sites"] = m365_max_sites
    if _is_commandline(ctx, "m365_max_teams"):
        m365_overrides["teams_max_teams"] = m365_max_teams
    if _is_commandline(ctx, "m365_max_drives"):
        m365_overrides["onedrive_max_drives"] = m365_max_drives
    if _is_commandline(ctx, "m365_max_mailboxes"):
        m365_overrides["exchange_max_mailboxes"] = m365_max_mailboxes
    if _is_commandline(ctx, "m365_filter_archived_teams"):
        m365_overrides["teams_filter_archived"] = m365_filter_archived_teams
    if m365_overrides:
        overrides["m365_config"] = m365_overrides

    # Purview overrides
    if _is_commandline(ctx, "include_purview"):
        overrides["include_purview"] = include_purview

    purview_overrides: dict = {}
    if _is_commandline(ctx, "purview_include_sensitivity_labels"):
        purview_overrides["include_sensitivity_labels"] = purview_include_sensitivity_labels
    if _is_commandline(ctx, "purview_include_dlp_policies"):
        purview_overrides["include_dlp_policies"] = purview_include_dlp_policies
    if _is_commandline(ctx, "purview_include_retention_policies"):
        purview_overrides["include_retention_policies"] = purview_include_retention_policies
    if _is_commandline(ctx, "purview_include_retention_labels"):
        purview_overrides["include_retention_labels"] = purview_include_retention_labels
    if _is_commandline(ctx, "purview_include_information_protection_policies"):
        purview_overrides["include_information_protection_policies"] = purview_include_information_protection_policies
    if _is_commandline(ctx, "purview_label_filter"):
        purview_overrides["sensitivity_label_filter"] = purview_label_filter
        purview_overrides["retention_label_filter"] = purview_label_filter
    if _is_commandline(ctx, "purview_policy_filter"):
        purview_overrides["dlp_policy_filter"] = purview_policy_filter
        purview_overrides["retention_policy_filter"] = purview_policy_filter
    if _is_commandline(ctx, "purview_max_labels"):
        purview_overrides["sensitivity_max_labels"] = purview_max_labels
        purview_overrides["retention_max_labels"] = purview_max_labels
    if _is_commandline(ctx, "purview_max_dlp_policies"):
        purview_overrides["dlp_max_policies"] = purview_max_dlp_policies
    if _is_commandline(ctx, "purview_max_retention_policies"):
        purview_overrides["retention_max_policies"] = purview_max_retention_policies
    if _is_commandline(ctx, "purview_include_disabled"):
        purview_overrides["dlp_include_disabled"] = purview_include_disabled
        purview_overrides["retention_include_disabled"] = purview_include_disabled
    if purview_overrides:
        overrides["purview_config"] = purview_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]

    # Compliance toggles
    for name in (
        "compliance_include_policy_assignments",
        "compliance_include_policy_states",
        "compliance_max_assignments_per_scope",
    ):
        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

    async def _run_with_panel() -> AzureDiscoveryResponse:
        # If quiet is True, we might still want the panel but with fewer logs,
        # or no panel at all? Usually quiet means no output.
        # But here let's assume if it's not a preview, we show the panel unless quiet implies absolutely no stderr either.
        # Typically 'quiet' suppresses INFO logs.

        with transient_live_panel("Initializing discovery...") as panel:
            # Redirect logs to panel
            logger = logging.getLogger("azure_discovery")
            # Clear existing handlers (set by enable_cli_logging)
            logger.handlers.clear()

            handler = PanelLogHandler(panel)
            # Use a simple format for the panel
            handler.setFormatter(logging.Formatter("[dim]%(name)s[/]: %(message)s"))
            logger.addHandler(handler)

            # If quiet, only show ERRORs in the panel (which might be red)
            logger.setLevel(logging.ERROR if quiet else logging.INFO)

            def update_status(msg: str) -> None:
                panel.set_status(msg)

            return await run_discovery(request, on_status_update=update_status)

    try:
        # Use rich transient panel if stderr is a TTY (interactive)
        if sys.stderr.isatty():
            response = asyncio.run(_run_with_panel())
        else:
            response = asyncio.run(run_discovery(request))
    except AzureClientError as exc:
        _emit_auth_failure_hint(request, exc)
        raise
    except Exception as exc:
        _emit_auth_failure_hint(request, exc)
        raise

    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")


@cli.command("init")
def init_command(
    output: Path = typer.Option(
        Path("azure-discovery-config.json"),
        "--output",
        "-o",
        path_type=Path,
        help="Output config file path.",
    ),
    format: str = typer.Option(
        "json",
        "--format",
        "-f",
        help="Output format: json or yaml (extension of --output overrides this if .json/.yaml/.yml).",
    ),
    tenant_id_placeholder: str = typer.Option(
        "YOUR_TENANT_ID",
        "--tenant-placeholder",
        help="Placeholder value for tenant_id when not using Azure CLI context.",
    ),
    use_az_context: bool = typer.Option(
        False,
        "--use-az-context",
        help="Pre-populate config from current 'az account' (tenant and optional subscription) without prompting.",
    ),
    no_prompt: bool = typer.Option(
        False,
        "--no-prompt",
        help="Do not ask to use Azure CLI context; use placeholder or --use-az-context only.",
    ),
) -> None:
    """Generate a config file with default values for all options.

    When run interactively, you can choose to pre-populate tenant_id (and optionally
    subscriptions) from your current Azure CLI context (az account show).
    Edit the output file and run: azure-discovery discover --config <path>
    """
    tenant_id = tenant_id_placeholder
    subscriptions: list[str] | None = None
    environment = AzureEnvironment.AZURE_PUBLIC
    used_az = False

    az_stderr = ""
    if use_az_context:
        az_ctx, az_stderr = _get_az_cli_context()
        if az_ctx:
            tenant_id = az_ctx.tenant_id
            if az_ctx.subscription_id:
                subscriptions = [az_ctx.subscription_id]
            if az_ctx.environment is not None:
                environment = az_ctx.environment
            used_az = True
        else:
            typer.echo("Azure CLI context not available (run 'az login'?). Using placeholder.", err=True)
            if az_stderr:
                typer.echo(az_stderr, err=True)
    elif sys.stdin.isatty() and not no_prompt:
        az_ctx, _ = _get_az_cli_context()
        if az_ctx:
            sub_msg = f", subscription: {az_ctx.subscription_id}" if az_ctx.subscription_id else ""
            prompt = (
                f"Use current Azure CLI context? (tenant: {az_ctx.tenant_id}{sub_msg})"
            )
            if click.confirm(prompt, default=True):
                tenant_id = az_ctx.tenant_id
                if az_ctx.subscription_id:
                    subscriptions = [az_ctx.subscription_id]
                if az_ctx.environment is not None:
                    environment = az_ctx.environment
                used_az = True

    request = AzureDiscoveryRequest(
        tenant_id=tenant_id,
        subscriptions=subscriptions,
        environment=environment,
    )
    data = request.model_dump(mode="json")

    suffix = output.suffix.lower()
    if suffix in (".yaml", ".yml"):
        out_format = "yaml"
    elif suffix == ".json":
        out_format = "json"
    else:
        out_format = format.lower()

    write_config_file(output, data, format=out_format)
    typer.echo(f"Config written to {output.absolute()}")
    subs_preview = (subscriptions or [])[:3]
    if subscriptions and len(subscriptions) > 3:
        subs_preview.append("...")
    subs_str = ", ".join(subs_preview) if subs_preview else "(none)"
    typer.echo(
        f"  tenant_id={tenant_id!r}, subscriptions=[{subs_str}], environment={environment.value}",
        err=True,
    )
    if used_az:
        typer.echo("Pre-populated from Azure CLI. Adjust options and run:", err=True)
    else:
        typer.echo("Set tenant_id and adjust options, then run:", err=True)
    typer.echo("  azure-discovery discover --config " + str(output), err=True)


def main() -> None:
    cli()


if __name__ == "__main__":
    main()
