from __future__ import annotations

import json
import os
from importlib import import_module

import typer

from svc_infra.apf_payments.alembic import discover_packages as payments_pkgs
from svc_infra.db.sql.core import current as core_current
from svc_infra.db.sql.core import downgrade as core_downgrade
from svc_infra.db.sql.core import history as core_history
from svc_infra.db.sql.core import init_alembic as core_init_alembic
from svc_infra.db.sql.core import merge_heads as core_merge_heads
from svc_infra.db.sql.core import revision as core_revision
from svc_infra.db.sql.core import setup_and_migrate as core_setup_and_migrate
from svc_infra.db.sql.core import stamp as core_stamp
from svc_infra.db.sql.core import upgrade as core_upgrade


def apply_database_url(database_url: str | None) -> None:
    """If provided, set SQL_URL for the current process."""
    if database_url:
        os.environ["SQL_URL"] = database_url


def _find_pkgs(with_payments, discover_packages) -> list[str]:
    from os import getenv

    payments_enabled = (
        with_payments
        if with_payments is not None
        else str(getenv("APF_ENABLE_PAYMENTS", "")).lower() in {"1", "true", "yes"}
    )
    final_pkgs = list(discover_packages or [])
    if payments_enabled:
        for p in payments_pkgs():
            if p not in final_pkgs:
                final_pkgs.append(p)

    return final_pkgs


def cmd_init(
    database_url: str | None = typer.Option(
        None,
        help="Database URL; overrides env SQL_URL for this command. "
        "Async vs sync is auto-detected from the URL.",
    ),
    discover_packages: list[str] | None = typer.Option(
        None,
        help="Packages to search for SQLAlchemy metadata; may pass multiple. "
        "If omitted, automatic discovery is used.",
    ),
    overwrite: bool = typer.Option(False, help="Overwrite existing files if present."),
    with_payments: bool = typer.Option(
        None,
        help="Include svc-infra payments models when rendering env.py. "
        "Defaults from env APF_ENABLE_PAYMENTS.",
    ),
):
    """
    Initialize Alembic scaffold. The env.py variant (async vs. sync) is
    auto-detected from SQL_URL (if available at init time).
    """
    final_pkgs = _find_pkgs(with_payments, discover_packages)
    apply_database_url(database_url)
    core_init_alembic(
        discover_packages=final_pkgs or None,
        overwrite=overwrite,
    )


def cmd_revision(
    message: str = typer.Option(..., "-m", "--message", help="Revision message."),
    database_url: str | None = typer.Option(
        None, help="Database URL; overrides env for this command."
    ),
    autogenerate: bool = typer.Option(False, help="Autogenerate migrations by comparing metadata."),
    head: str | None = typer.Option("head", help="Set the head to base this revision on."),
    branch_label: str | None = typer.Option(None, help="Branch label."),
    version_path: str | None = typer.Option(None, help="Alternative versions/ path."),
    sql: bool = typer.Option(False, help="Don't generate Python; dump SQL to stdout."),
):
    """Create a new Alembic revision, either empty or autogenerated."""
    apply_database_url(database_url)
    core_revision(
        message=message,
        autogenerate=autogenerate,
        head=head,
        branch_label=branch_label,
        version_path=version_path,
        sql=sql,
    )


def cmd_upgrade(
    revision_target: str = typer.Argument("head", help="Target revision (default head)."),
    database_url: str | None = typer.Option(
        None, help="Database URL; overrides env for this command."
    ),
):
    """Upgrade to a later version."""
    apply_database_url(database_url)
    core_upgrade(revision_target=revision_target)


def cmd_downgrade(
    revision_target: str = typer.Argument("-1", help="Target revision (default -1)."),
    database_url: str | None = typer.Option(
        None, help="Database URL; overrides env for this command."
    ),
):
    """Downgrade to an earlier version."""
    apply_database_url(database_url)
    core_downgrade(revision_target=revision_target)


def cmd_current(
    database_url: str | None = typer.Option(
        None, help="Database URL; overrides env for this command."
    ),
    verbose: bool = typer.Option(False, help="Verbose output."),
):
    """Display the current revision for each database."""
    apply_database_url(database_url)
    result = core_current(verbose=verbose)
    try:
        typer.echo(json.dumps(result))
    except Exception:
        typer.echo(str(result))


def cmd_history(
    database_url: str | None = typer.Option(
        None, help="Database URL; overrides env for this command."
    ),
    verbose: bool = typer.Option(False, help="Verbose output."),
):
    """List changeset scripts in chronological order."""
    apply_database_url(database_url)
    core_history(verbose=verbose)


def cmd_stamp(
    revision_target: str = typer.Argument("head"),
    database_url: str | None = typer.Option(
        None, help="Database URL; overrides env for this command."
    ),
):
    """Stamp the revision table with the given revision; don't run migrations."""
    apply_database_url(database_url)
    core_stamp(revision_target=revision_target)


def cmd_merge_heads(
    database_url: str | None = typer.Option(
        None, help="Database URL; overrides env for this command."
    ),
    message: str | None = typer.Option(None, "-m", "--message", help="Merge revision message."),
):
    """Create a merge revision for multiple heads."""
    apply_database_url(database_url)
    core_merge_heads(message=message)


def cmd_setup_and_migrate(
    database_url: str | None = typer.Option(
        None,
        help="Overrides env for this command. Async vs sync is auto-detected from the URL.",
    ),
    overwrite_scaffold: bool = typer.Option(False, help="Overwrite alembic scaffold if present."),
    create_db_if_missing: bool = typer.Option(True, help="Create the database/schema if missing."),
    create_followup_revision: bool = typer.Option(
        True, help="Create an autogen follow-up revision if revisions already exist."
    ),
    initial_message: str = typer.Option("initial schema"),
    followup_message: str = typer.Option("autogen"),
    # NEW:
    discover_packages: list[str] | None = typer.Option(
        None,
        help="Packages Alembic should import to discover models "
        "(e.g. app.models,svc_infra.apf_payments.models).",
    ),
    with_payments: bool = typer.Option(
        None,  # None = read env
        help="Include svc-infra payments models in migrations. "
        "If omitted, falls back to env APF_ENABLE_PAYMENTS=true/1.",
    ),
):
    """
    End-to-end: ensure DB exists, scaffold Alembic, create/upgrade, all in one command.
    Async vs. sync is inferred from SQL_URL.
    """
    final_pkgs = _find_pkgs(with_payments, discover_packages)
    result = core_setup_and_migrate(
        overwrite_scaffold=overwrite_scaffold,
        create_db_if_missing=create_db_if_missing,
        create_followup_revision=create_followup_revision,
        initial_message=initial_message,
        followup_message=followup_message,
        discover_packages=final_pkgs or None,
        database_url=database_url,
    )
    # Echo a concise JSON result so tests and users can introspect outcome
    try:
        typer.echo(json.dumps(result))
    except Exception:
        # Fallback to plain string if not JSON-serializable for any reason
        typer.echo(str(result))


def register(app: typer.Typer) -> None:
    # Register under the 'sql' group app
    app.command("init")(cmd_init)
    app.command("revision")(cmd_revision)
    app.command("upgrade")(cmd_upgrade)
    # Allow unknown options so users can pass "-1" like Alembic without Click treating it as an option
    app.command(
        "downgrade",
        context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
    )(cmd_downgrade)
    app.command("current")(cmd_current)
    app.command("history")(cmd_history)
    app.command("stamp")(cmd_stamp)
    app.command("merge-heads")(cmd_merge_heads)
    app.command("setup-and-migrate")(cmd_setup_and_migrate)
    app.command("seed")(cmd_seed)


def _import_callable(path: str):
    mod_name, _, fn_name = path.partition(":")
    if not mod_name or not fn_name:
        raise typer.BadParameter("Expected format 'module.path:callable'")
    # Back-compat: after moving tests under tests/unit, allow legacy test module
    # dotted paths like 'tests.db.sql.test_sql_seed_cli:my_seed'.
    mod = None
    unit_mod = None
    if mod_name.startswith("tests.db."):
        # Try legacy import first (shim module), then unit module fallback
        try:
            mod = import_module(mod_name)
        except ModuleNotFoundError:
            pass
        unit_name = mod_name.replace("tests.db.", "tests.unit.db.", 1)
        try:
            unit_mod = import_module(unit_name)
        except ModuleNotFoundError:
            unit_mod = None
        # If both exist, unify shared state where applicable
        if mod is not None and unit_mod is not None:
            # Example: tests use a global `called` dict; point legacy to unit
            try:
                if hasattr(unit_mod, "called"):
                    mod.called = unit_mod.called  # type: ignore[attr-defined]
            except Exception:
                pass
        # If legacy mod missing but unit exists, use unit
        if mod is None and unit_mod is not None:
            mod = unit_mod
    else:
        mod = import_module(mod_name)
    fn = getattr(mod, fn_name, None)
    if not callable(fn):
        raise typer.BadParameter(f"Callable '{fn_name}' not found in module '{mod_name}'")
    return fn


def cmd_seed(
    target: str = typer.Argument(..., help="Seed callable path 'module:func'"),
    database_url: str | None = typer.Option(
        None,
        help="Database URL; overrides env for this command.",
    ),
):
    """Run a user-provided seed function to load fixtures/reference data."""
    apply_database_url(database_url)
    fn = _import_callable(target)
    fn()
