import asyncio
import logging
import os
from urllib.parse import urlparse

import click
from relay import __version__, misc
from relay.application import (
    Application,
    request_id_middleware,
)


@click.group(
    "cli", context_settings={"show_default": True}, invoke_without_command=True
)
@click.option("--config", "-c", default="relay.yaml", help="path to the relay's config")
@click.version_option(version=__version__, prog_name="ActivityRelay")
@click.pass_context
def cli(ctx, config):
    ctx.obj = Application(config, middlewares=[request_id_middleware])

    level = {
        "debug": logging.DEBUG,
        "info": logging.INFO,
        "error": logging.ERROR,
        "critical": logging.CRITICAL,
        "fatal": logging.FATAL,
    }.get(os.getenv("LOG_LEVEL", "INFO").lower(), logging.INFO)

    logging.basicConfig(
        level=level,
        format="[%(asctime)s] %(levelname)s: %(message)s",
    )


@cli.group("inbox")
@click.pass_obj
def cli_inbox(ctx: Application):
    "Manage the inboxes in the database"


@cli_inbox.command("list")
@click.pass_obj
def cli_inbox_list(obj: Application):
    "List the connected instances or relays"

    click.echo("Connected to the following instances or relays:")

    for inbox in obj.database.inboxes:
        click.echo(f"- {inbox}")


@cli_inbox.command("follow")
@click.argument("actor")
@click.pass_obj
def cli_inbox_follow(obj: Application, actor):
    "Follow an actor (Relay must be running)"

    if obj.config.is_banned(actor):
        return click.echo(f"Error: Refusing to follow banned actor: {actor}")

    if not actor.startswith("http"):
        domain = actor
        actor = f"https://{actor}/actor"

    else:
        domain = urlparse(actor).hostname

    try:
        inbox_data = obj.database["relay-list"][domain]
        inbox = inbox_data["inbox"]

    except KeyError:
        actor_data = asyncio.run(misc.request(actor))
        inbox = actor_data.shared_inbox

    message = misc.Message.new_follow(host=obj.config.host, actor=actor.id)

    asyncio.run(misc.request(inbox, message))
    click.echo(f"Sent follow message to actor: {actor}")


@cli_inbox.command("unfollow")
@click.argument("actor")
@click.pass_obj
def cli_inbox_unfollow(obj: Application, actor):
    "Unfollow an actor (Relay must be running)"

    if not actor.startswith("http"):
        domain = actor
        actor = f"https://{actor}/actor"

    else:
        domain = urlparse(actor).hostname

    try:
        inbox_data = obj.database["relay-list"][domain]
        inbox = inbox_data["inbox"]
        message = misc.Message.new_unfollow(
            host=obj.config.host, actor=actor, follow=inbox_data["followid"]
        )

    except KeyError:
        actor_data = asyncio.run(misc.request(actor))
        inbox = actor_data.shared_inbox
        message = misc.Message.new_unfollow(
            host=obj.config.host,
            actor=actor,
            follow={
                "type": "Follow",
                "object": actor,
                "actor": f"https://{obj.config.host}/actor",
            },
        )

    asyncio.run(misc.request(inbox, message))
    click.echo(f"Sent unfollow message to: {actor}")


@cli_inbox.command("add")
@click.argument("inbox")
@click.pass_obj
def cli_inbox_add(obj: Application, inbox):
    "Add an inbox to the database"

    if not inbox.startswith("http"):
        inbox = f"https://{inbox}/inbox"

    if obj.config.is_banned(inbox):
        return click.echo(f"Error: Refusing to add banned inbox: {inbox}")

    if obj.database.add_inbox(inbox):
        obj.database.save()
        return click.echo(f"Added inbox to the database: {inbox}")

    click.echo(f"Error: Inbox already in database: {inbox}")


@cli_inbox.command("remove")
@click.argument("inbox")
@click.pass_obj
def cli_inbox_remove(obj: Application, inbox):
    "Remove an inbox from the database"

    try:
        dbinbox = obj.database.get_inbox(inbox, fail=True)

    except KeyError:
        click.echo(f"Error: Inbox does not exist: {inbox}")
        return

    obj.database.del_inbox(dbinbox["domain"])
    obj.database.save()

    click.echo(f"Removed inbox from the database: {inbox}")


@cli.group("instance")
def cli_instance():
    "Manage instance bans"


@cli_instance.command("list")
@click.pass_obj
def cli_instance_list(obj: Application):
    "List all banned instances"

    click.echo("Banned instances or relays:")

    for domain in obj.config.blocked_instances:
        click.echo(f"- {domain}")


@cli_instance.command("ban")
@click.argument("target")
@click.pass_obj
def cli_instance_ban(obj: Application, target):
    "Ban an instance and remove the associated inbox if it exists"

    if target.startswith("http"):
        target = urlparse(target).hostname

    if obj.config.ban_instance(target):
        obj.config.save()

        if obj.database.del_inbox(target):
            obj.database.save()

        click.echo(f"Banned instance: {target}")
        return

    click.echo(f"Instance already banned: {target}")


@cli_instance.command("unban")
@click.argument("target")
@click.pass_obj
def cli_instance_unban(obj: Application, target):
    "Unban an instance"

    if obj.config.unban_instance(target):
        obj.config.save()

        click.echo(f"Unbanned instance: {target}")
        return

    click.echo(f"Instance wasn't banned: {target}")


@cli.group("software")
def cli_software():
    "Manage banned software"


@cli_software.command("list")
@click.pass_obj
def cli_software_list(obj: Application):
    "List all banned software"

    click.echo("Banned software:")

    for software in obj.config.blocked_software:
        click.echo(f"- {software}")


@cli_software.command("ban")
@click.option(
    "--fetch-nodeinfo/--ignore-nodeinfo",
    "-f",
    "fetch_nodeinfo",
    default=False,
    help="Treat NAME like a domain and try to fet the software name from nodeinfo",
)
@click.argument("name")
@click.pass_obj
def cli_software_ban(obj: Application, name, fetch_nodeinfo):
    "Ban software. Use RELAYS for NAME to ban relays"

    if fetch_nodeinfo:
        software = asyncio.run(misc.fetch_nodeinfo(name))

        if not software:
            click.echo(f"Failed to fetch software name from domain: {name}")

        name = software

    if config.ban_software(name):
        obj.config.save()
        return click.echo(f"Banned software: {name}")

    click.echo(f"Software already banned: {name}")


@cli_software.command("unban")
@click.option(
    "--fetch-nodeinfo/--ignore-nodeinfo",
    "-f",
    "fetch_nodeinfo",
    default=False,
    help="Treat NAME like a domain and try to fet the software name from nodeinfo",
)
@click.argument("name")
@click.pass_obj
def cli_software_unban(obj: Application, name, fetch_nodeinfo):
    "Ban software. Use RELAYS for NAME to unban relays"

    if fetch_nodeinfo:
        software = asyncio.run(misc.fetch_nodeinfo(name))

        if not software:
            click.echo(f"Failed to fetch software name from domain: {name}")

        name = software

    if obj.config.unban_software(name):
        obj.config.save()
        return click.echo(f"Unbanned software: {name}")

    click.echo(f"Software wasn't banned: {name}")


@cli.group("whitelist")
def cli_whitelist():
    "Manage the instance whitelist"


@cli_whitelist.command("list")
@click.pass_obj
def cli_whitelist_list(obj: Application):
    click.echo("Current whitelisted domains")

    for domain in obj.config.whitelist:
        click.echo(f"- {domain}")


@cli_whitelist.command("add")
@click.argument("instance")
@click.pass_obj
def cli_whitelist_add(obj: Application, instance):
    "Add an instance to the whitelist"

    if not obj.config.add_whitelist(instance):
        return click.echo(f"Instance already in the whitelist: {instance}")

    obj.config.save()
    click.echo(f"Instance added to the whitelist: {instance}")


@cli_whitelist.command("remove")
@click.argument("instance")
@click.pass_obj
def cli_whitelist_remove(obj: Application, instance):
    "Remove an instance from the whitelist"

    if not obj.config.del_whitelist(instance):
        return click.echo(f"Instance not in the whitelist: {instance}")

    obj.config.save()

    if obj.config.whitelist_enabled:
        if obj.database.del_inbox(inbox):
            obj.database.save()

    click.echo(f"Removed instance from the whitelist: {instance}")


@cli.command("setup")
@click.pass_obj
def relay_setup(obj: Application):
    "Generate a new config"

    if not obj.config.is_docker:
        while True:
            obj.config.host = os.getenv("RELAY_HOSTNAME") or click.prompt(
                "What domain will the relay be hosted on?", default=obj.config.host
            )

            if not obj.config.host.endswith("example.com"):
                break

            click.echo("The domain must not be example.com")

        obj.config.listen = os.getenv("LISTEN_ADDRESS") or click.prompt(
            "Which address should the relay listen on?", default=obj.config.listen
        )

        while True:
            obj.config.port = click.prompt(
                "What TCP port should the relay listen on?",
                default=obj.config.port,
                type=int,
            )
            break

    else:
        obj.config.listen = os.getenv("LISTEN_ADDRESS", obj.config.listen)
        obj.config.port = int(os.getenv("LISTEN_PORT", obj.config.port))
        obj.config.host = os.getenv("RELAY_HOSTNAME")
        obj.config.admin_token = os.getenv("RELAY_ADMIN_TOKEN")
        if not obj.config.host:
            click.echo("Error: No relay host configured! Set $RELAY_HOSTNAME")
            exit(1)

    obj.config.save()

    if not obj.config.is_docker and click.confirm(
        "Relay all setup! Would you like to run it now?"
    ):
        relay_run.callback()


@cli.command("run")
@click.pass_obj
def relay_run(obj: Application):
    "Run the relay"

    if obj.config.host.endswith("example.com"):
        return click.echo(
            'Relay is not set up. Please edit your relay config or run "activityrelay setup".'
        )

    if not misc.check_open_port(obj.config.listen, obj.config.port):
        return click.echo(
            f"Error: A server is already running on port {obj.config.port}"
        )

    obj.run()


if __name__ == "__main__":
    cli(prog_name="relay")
