"""Looker CLI commands for g-gremlin."""

from __future__ import annotations

import json
from typing import Any, Dict, List, Optional

import click

from g_gremlin.credentials.provider import CredentialProvider
from g_gremlin.looker.client import LookerClient
from g_gremlin.looker.errors import LookerSpecError
from g_gremlin.looker.spec_loader import (
    load_dashboard_spec,
    load_filter_spec,
    load_look_spec,
    load_spec_data,
    load_tile_spec,
)


def _emit_json(payload: Any) -> None:
    click.echo(json.dumps(payload, indent=2, default=str))


def _handle_client_error(exc: Exception) -> None:
    raise click.ClickException(str(exc))


def _get_looker_client() -> LookerClient:
    return LookerClient()


def _parse_key_value(pairs: List[str], *, label: str) -> Dict[str, str]:
    out: Dict[str, str] = {}
    for item in pairs:
        if "=" not in item:
            raise click.ClickException(f"Invalid {label} '{item}'. Use key=value.")
        key, value = item.split("=", 1)
        key = key.strip()
        if not key:
            raise click.ClickException(f"Invalid {label} '{item}'. Empty key.")
        out[key] = value
    return out


def _build_query_payload(
    *,
    spec: Optional[str],
    model: Optional[str],
    explore: Optional[str],
    fields: List[str],
    filters: List[str],
    sorts: List[str],
    limit: Optional[int],
) -> Dict[str, Any]:
    if spec:
        payload = load_spec_data(spec)
        if isinstance(payload.get("query"), dict):
            return payload["query"]
        return payload

    if not model or not explore or not fields:
        raise click.ClickException("Provide --spec or --model --explore with at least one --field")

    query_payload: Dict[str, Any] = {
        "model": model,
        "explore": explore,
        "fields": list(fields),
    }
    if filters:
        query_payload["filters"] = _parse_key_value(filters, label="filter")
    if sorts:
        query_payload["sorts"] = list(sorts)
    if limit is not None:
        query_payload["limit"] = limit
    return query_payload


@click.group()
def looker() -> None:
    """Looker helpers for query, dashboard provisioning, and iteration."""


@looker.command("connect")
@click.option("--base-url", prompt=True, help="Looker base URL, e.g. https://acme.cloud.looker.com")
@click.option("--client-id", prompt=True, help="Looker API3 client id.")
@click.option("--client-secret", prompt=True, hide_input=True, help="Looker API3 client secret.")
@click.option("--api-version", default="4.0", show_default=True, help="Looker API version segment.")
@click.option(
    "--verify-ssl/--no-verify-ssl",
    default=True,
    show_default=True,
    help="Enable/disable TLS certificate verification.",
)
def connect(
    base_url: str,
    client_id: str,
    client_secret: str,
    api_version: str,
    verify_ssl: bool,
) -> None:
    """Store Looker credentials and validate authentication."""
    provider = CredentialProvider()
    if not provider.has_keychain_backend():
        raise click.ClickException(
            "No keychain backend available. Keyring is installed but no OS credential store was detected."
        )

    client = LookerClient(
        base_url=base_url,
        client_id=client_id,
        client_secret=client_secret,
        api_version=api_version,
        verify_ssl=verify_ssl,
    )
    try:
        client.sign_in()
    except Exception as exc:
        _handle_client_error(exc)

    provider.set_secret("looker", "base_url", base_url.strip())
    provider.set_secret("looker", "client_id", client_id.strip())
    provider.set_secret("looker", "client_secret", client_secret.strip())
    provider.set_secret("looker", "api_version", str(api_version).strip())
    provider.set_secret("looker", "verify_ssl", "true" if verify_ssl else "false")
    click.echo("Looker credentials stored in keychain.")


@looker.command("disconnect")
def disconnect() -> None:
    """Remove stored Looker credentials."""
    provider = CredentialProvider()
    removed = False
    for key in ["base_url", "client_id", "client_secret", "api_version", "verify_ssl"]:
        if provider.delete_secret("looker", key):
            removed = True
            click.echo(f"Removed looker.{key}")
    if not removed:
        click.echo("No Looker secrets found")


@looker.command("doctor")
@click.option("--write-check", is_flag=True, help="Attempt create/delete probe dashboard in --space-id.")
@click.option("--space-id", default=None, help="Space id used by --write-check.")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def doctor(write_check: bool, space_id: str | None, json_output: bool) -> None:
    """Run Looker auth and capability checks."""
    client = _get_looker_client()
    try:
        report = client.doctor(write_check=write_check, space_id=space_id)
    except Exception as exc:
        _handle_client_error(exc)
        return

    if json_output:
        _emit_json(report)
        return

    click.echo(f"Status: {report.get('status')}")
    click.echo(f"Auth: {report.get('auth')}")
    click.echo(f"API version: {report.get('api_version')}")
    click.echo(f"Models: {report.get('models')}")
    click.echo(f"Spaces: {report.get('spaces')}")
    click.echo(f"Write check: {report.get('write_check')}")


@looker.group()
def describe() -> None:
    """Progressive model/explore/field introspection."""


@describe.command("models")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def describe_models(json_output: bool) -> None:
    client = _get_looker_client()
    try:
        models = client.list_models()
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(models)
    else:
        for item in models:
            click.echo(f"{item.get('name')}\t{item.get('label') or ''}")


@describe.command("explores")
@click.option("--model", required=True, help="LookML model name.")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def describe_explores(model: str, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        explores = client.list_explores(model)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(explores)
    else:
        for item in explores:
            click.echo(f"{item.get('name')}\t{item.get('label') or ''}")


@describe.command("fields")
@click.option("--model", required=True, help="LookML model name.")
@click.option("--explore", required=True, help="Explore name.")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def describe_fields(model: str, explore: str, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        payload = client.describe_fields(model, explore)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(payload)
    else:
        dims = len(payload.get("fields", {}).get("dimensions", []) or [])
        meas = len(payload.get("fields", {}).get("measures", []) or [])
        click.echo(f"model={model} explore={explore} dimensions={dims} measures={meas}")


@looker.command("query")
@click.option("--spec", default=None, help="Query spec file (JSON/YAML).")
@click.option("--model", default=None, help="LookML model name.")
@click.option("--explore", default=None, help="Explore name.")
@click.option("--field", "fields", multiple=True, help="Field to include. Repeat for multiple.")
@click.option("--filter", "filters", multiple=True, help="Filter as key=value. Repeat for multiple.")
@click.option("--sort", "sorts", multiple=True, help="Sort expression. Repeat for multiple.")
@click.option("--limit", default=None, type=int, help="Query row limit.")
@click.option(
    "--format",
    "result_format",
    default="json",
    show_default=True,
    type=click.Choice(["json", "json_detail", "csv", "txt"], case_sensitive=False),
    help="Looker result format.",
)
@click.option("--json", "json_output", is_flag=True, help="Force JSON output envelope.")
def query(
    spec: str | None,
    model: str | None,
    explore: str | None,
    fields: tuple[str, ...],
    filters: tuple[str, ...],
    sorts: tuple[str, ...],
    limit: int | None,
    result_format: str,
    json_output: bool,
) -> None:
    """Run an inline Looker query."""
    query_payload = _build_query_payload(
        spec=spec,
        model=model,
        explore=explore,
        fields=list(fields),
        filters=list(filters),
        sorts=list(sorts),
        limit=limit,
    )
    client = _get_looker_client()
    try:
        result = client.run_inline_query(query_payload, result_format=result_format)
    except Exception as exc:
        _handle_client_error(exc)
        return

    if json_output or result_format in {"json", "json_detail"}:
        _emit_json(result)
    else:
        click.echo(result)


@looker.group()
def look() -> None:
    """Look management commands."""


@look.command("list")
@click.option("--space-id", default=None, help="Filter by space id.")
@click.option("--title", default=None, help="Filter by title.")
@click.option("--limit", default=100, show_default=True, type=int, help="Maximum looks to return.")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def look_list(space_id: str | None, title: str | None, limit: int, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        looks = client.list_looks(space_id=space_id, title=title, limit=limit)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(looks)
    else:
        for item in looks:
            click.echo(f"{item.get('id')}\t{item.get('title')}")


@look.command("create")
@click.option("--spec", required=True, help="Look spec file (LookerLookSpecV1).")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def look_create(spec: str, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        look_spec = load_look_spec(spec)
        created = client.create_look(look_spec)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(created)
    else:
        click.echo(f"Created look {created.get('id')}: {created.get('title')}")


@looker.group()
def dashboard() -> None:
    """Dashboard inventory, provisioning, and iterative edits."""


@dashboard.command("list")
@click.option("--space-id", default=None, help="Filter by space id.")
@click.option("--title", default=None, help="Filter by title.")
@click.option("--limit", default=100, show_default=True, type=int, help="Maximum dashboards to return.")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def dashboard_list(space_id: str | None, title: str | None, limit: int, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        dashboards = client.list_dashboards(space_id=space_id, title=title, limit=limit)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(dashboards)
    else:
        for item in dashboards:
            click.echo(f"{item.get('id')}\t{item.get('title')}")


@dashboard.command("describe")
@click.option("--dashboard-id", required=True, help="Dashboard id.")
@click.option(
    "--emit",
    default="spec",
    show_default=True,
    type=click.Choice(["spec", "raw"], case_sensitive=False),
    help="Emit normalized spec or raw API payload.",
)
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def dashboard_describe(dashboard_id: str, emit: str, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        if emit == "raw":
            payload = client.get_dashboard(dashboard_id, hydrate_queries=True)
        else:
            payload = client.describe_dashboard_spec(dashboard_id)
    except Exception as exc:
        _handle_client_error(exc)
        return

    if json_output:
        _emit_json(payload)
    else:
        if emit == "raw":
            click.echo(f"dashboard_id={payload.get('id')} title={payload.get('title')}")
        else:
            click.echo(
                f"spec_version={payload.get('spec_version')} title={payload.get('dashboard', {}).get('title')}"
            )


@dashboard.command("create")
@click.option("--spec", required=True, help="Dashboard spec file (LookerDashboardSpecV1).")
@click.option(
    "--mode",
    default=None,
    type=click.Choice(["merge", "replace"], case_sensitive=False),
    help="Apply mode override. Defaults to spec.apply_mode or merge.",
)
@click.option("--force", is_flag=True, help="Required for replace mode.")
@click.option("--dry-run", is_flag=True, help="Show mutation plan without applying.")
@click.option("--skip-ref-check", is_flag=True, help="Skip model/explore reference validation.")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def dashboard_create(
    spec: str,
    mode: str | None,
    force: bool,
    dry_run: bool,
    skip_ref_check: bool,
    json_output: bool,
) -> None:
    """Create or update (upsert) a dashboard from spec."""
    client = _get_looker_client()
    try:
        dashboard_spec = load_dashboard_spec(spec)
        result = client.apply_dashboard_spec(
            dashboard_spec,
            mode=mode or dashboard_spec.apply_mode or "merge",
            dry_run=dry_run,
            force=force,
            validate_refs=not skip_ref_check,
        )
    except LookerSpecError as exc:
        _handle_client_error(exc)
        return
    except Exception as exc:
        _handle_client_error(exc)
        return

    if json_output:
        _emit_json(result)
    else:
        click.echo(
            f"action={result.get('action')} mode={result.get('mode')} dry_run={result.get('dry_run')} "
            f"dashboard_id={result.get('dashboard_id')}"
        )


@dashboard.command("add-tile")
@click.option("--dashboard-id", required=True, help="Dashboard id.")
@click.option("--spec", required=True, help="Tile spec file (LookerTileSpecV1).")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def dashboard_add_tile(dashboard_id: str, spec: str, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        tile = load_tile_spec(spec)
        created = client.add_tile(dashboard_id, tile)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(created)
    else:
        click.echo(f"Created tile {created.get('id')} on dashboard {dashboard_id}")


@dashboard.command("add-filter")
@click.option("--dashboard-id", required=True, help="Dashboard id.")
@click.option("--spec", required=True, help="Filter spec file (LookerFilterSpecV1).")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def dashboard_add_filter(dashboard_id: str, spec: str, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        filt = load_filter_spec(spec)
        created = client.add_filter(dashboard_id, filt)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(created)
    else:
        click.echo(f"Created filter {created.get('id')} on dashboard {dashboard_id}")


@dashboard.command("remove-tile")
@click.option("--dashboard-id", required=True, help="Dashboard id.")
@click.option("--element-id", required=True, help="Dashboard element id.")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def dashboard_remove_tile(dashboard_id: str, element_id: str, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        result = client.remove_tile(dashboard_id, element_id)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(result)
    else:
        click.echo(f"Removed tile {element_id} from dashboard {dashboard_id}")


@dashboard.command("remove-filter")
@click.option("--dashboard-id", required=True, help="Dashboard id.")
@click.option("--filter-id", required=True, help="Dashboard filter id.")
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
def dashboard_remove_filter(dashboard_id: str, filter_id: str, json_output: bool) -> None:
    client = _get_looker_client()
    try:
        result = client.remove_filter(dashboard_id, filter_id)
    except Exception as exc:
        _handle_client_error(exc)
        return
    if json_output:
        _emit_json(result)
    else:
        click.echo(f"Removed filter {filter_id} from dashboard {dashboard_id}")


__all__ = ["looker"]
