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

from __future__ import annotations

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

import click
import typer
from click.core import ParameterSource

try:
    from rich_transient import transient_live_panel
except ImportError:
    from contextlib import contextmanager
    from typing import Iterator

    @contextmanager
    def transient_live_panel(_msg: str) -> Iterator[Any]:
        """No-op fallback when rich_transient is not installed."""
        yield type("_Panel", (), {"set_status": lambda self, _: None})()
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


# 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_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)",
    ),
    # 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_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

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

    # Use rich transient panel if stderr is a TTY (interactive)
    # If not interactive (piping), stick to standard logging
    try:
        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()
