from collections.abc import Iterable
from typing import Any, Optional

import click
import pgtoolkit.conf

from .. import instances
from ..ctx import Context
from ..models import system
from ..types import ConfigChanges
from .util import Group, instance_identifier_option, pass_ctx, pass_instance


@click.group(cls=Group)
@instance_identifier_option
def cli(**kwargs: Any) -> None:
    """Manage configuration of a PostgreSQL instance."""


def show_configuration_changes(
    changes: ConfigChanges, parameters: Optional[Iterable[str]] = None
) -> None:
    for param, (old, new) in changes.items():
        click.secho(f"{param}: {old} -> {new}", err=True, fg="green")
    if parameters is None:
        return
    if unchanged := set(parameters) - set(changes):
        click.secho(
            f"changes in {', '.join(map(repr, sorted(unchanged)))} not applied",
            err=True,
            fg="red",
        )
        click.secho(
            " hint: either these changes have no effect (values already set) "
            "or specified parameters are already defined in an un-managed file "
            "(e.g. 'postgresql.conf')",
            err=True,
            fg="blue",
        )


@cli.command("show")
@click.argument("parameter", nargs=-1)
@pass_instance
@pass_ctx
def show(ctx: Context, instance: system.Instance, parameter: tuple[str]) -> None:
    """Show configuration (all parameters or specified ones).

    Only uncommented parameters are shown when no PARAMETER is specified. When
    specific PARAMETERs are queried, commented values are also shown.
    """
    config = instance.config()
    for entry in config.entries.values():
        if parameter:
            if entry.name in parameter:
                if entry.commented:
                    click.echo(f"# {entry.name} = {entry.serialize()}")
                else:
                    click.echo(f"{entry.name} = {entry.serialize()}")
        elif not entry.commented:
            click.echo(f"{entry.name} = {entry.serialize()}")


def validate_configuration_parameters(
    context: click.Context, param: click.Parameter, value: tuple[str]
) -> dict[str, str]:
    items = {}
    for v in value:
        try:
            key, val = v.split("=", 1)
        except ValueError:
            raise click.BadParameter(v) from None
        items[key] = val
    return items


@cli.command("set")
@click.argument(
    "parameters",
    metavar="<PARAMETER>=<VALUE>...",
    nargs=-1,
    callback=validate_configuration_parameters,
    required=True,
)
@pass_instance
@pass_ctx
def set_(ctx: Context, instance: system.Instance, parameters: dict[str, Any]) -> None:
    """Set configuration items."""
    with ctx.lock:
        manifest = instances._get(ctx, instance)
        manifest.settings.update(parameters)
        changes = instances.configure(ctx, manifest)
        show_configuration_changes(changes, parameters.keys())


@cli.command("remove")
@click.argument("parameters", nargs=-1, required=True)
@pass_instance
@pass_ctx
def remove(ctx: Context, instance: system.Instance, parameters: tuple[str]) -> None:
    """Remove configuration items."""
    with ctx.lock:
        manifest = instances._get(ctx, instance)
        for p in parameters:
            try:
                del manifest.settings[p]
            except KeyError:
                raise click.ClickException(
                    f"{p!r} not found in managed configuration"
                ) from None
        changes = instances.configure(ctx, manifest)
        show_configuration_changes(changes, parameters)


@cli.command("edit")
@pass_instance
@pass_ctx
def edit(ctx: Context, instance: system.Instance) -> None:
    """Edit managed configuration."""
    with ctx.lock:
        actual_config = ctx.hook.postgresql_editable_conf(ctx=ctx, instance=instance)
        edited = click.edit(text=actual_config)
        if edited is None:
            click.echo("no change", err=True)
            return
        config = pgtoolkit.conf.parse_string(edited)
        values = config.as_dict()
        manifest = instances._get(ctx, instance)
        manifest.settings.clear()
        manifest.settings.update(values)
        changes = instances.configure(ctx, manifest)
        show_configuration_changes(changes)
