"""Command line tool for aiobaf6."""
from __future__ import annotations

import argparse
import asyncio
import difflib
import ipaddress
import logging.config
import sys
import typing as t

from google.protobuf import text_format
from zeroconf import IPVersion
from zeroconf.asyncio import AsyncZeroconf

from aiobafi6 import wireutils
from aiobafi6.device import Device
from aiobafi6.discovery import PORT, Service, ServiceBrowser
from aiobafi6.proto import aiobafi6_pb2

ARGS = argparse.ArgumentParser(
    description="Command line tool for aiobaf6.\n\nThe tool supports a direct connection mode that is more powerful for debugging."  # pylint: disable=line-too-long
)
ARGS.add_argument(
    "-s",
    "--discover",
    action="store_true",
    dest="discover",
    help="discover devices",
)
ARGS.add_argument(
    "-i",
    "--ip",
    action="store",
    dest="ip_addr",
    help="device address",
)
ARGS.add_argument(
    "-d",
    "--dump",
    action="store_true",
    dest="dump",
    help="enable proto dumping",
)
ARGS.add_argument(
    "-r",
    "--direct",
    action="store_true",
    dest="direct",
    help="directly connect to device, bypassing library",
)
ARGS.add_argument(
    "-t",
    "--interval",
    action="store",
    dest="interval",
    default=15,
    type=int,
    help="property query interval in seconds",
)
ARGS.add_argument(
    "property",
    nargs="?",
    help="property name",
)
ARGS.add_argument(
    "value",
    nargs="?",
    help="property value",
)

LOGGING = {
    "version": 1,
    "disable_existing_loggers": True,
    "formatters": {
        "simple": {"format": "%(levelname)s:%(asctime)s:%(name)s: %(message)s"},
    },
    "handlers": {
        "console": {
            "level": "DEBUG",
            "class": "logging.StreamHandler",
            "formatter": "simple",
        },
    },
    "loggers": {
        "": {
            "level": "INFO",
            "handlers": ["console"],
        },
        "aiobafi6.device": {
            "level": "DEBUG",
        },
        "aiobafi6.discovery": {
            "level": "DEBUG",
        },
    },
}


async def query_loop(writer: asyncio.StreamWriter, interval: int):
    """Run direct query loop."""
    print(f"starting query loop: interval {interval}s")
    root = aiobafi6_pb2.Root()  # pylint: disable=no-member
    root.root2.query.property_query = aiobafi6_pb2.ALL  # pylint: disable=no-member
    writer.write(wireutils.serialize(root))
    while True:
        await asyncio.sleep(interval)
        print("sending refresh query")
        root = aiobafi6_pb2.Root()  # pylint: disable=no-member
        root.root2.query.property_query = aiobafi6_pb2.ALL  # pylint: disable=no-member
        writer.write(wireutils.serialize(root))


async def direct_query_state(ip_addr: str, dump: bool, interval: int):
    """Query state directly."""
    print(f"directly querying all state from {ip_addr}")
    reader, writer = await asyncio.open_connection(ip_addr, PORT)
    _ = asyncio.create_task(query_loop(writer, interval))
    i = 0
    previous = aiobafi6_pb2.Properties()  # pylint: disable=no-member
    latest = aiobafi6_pb2.Properties()  # pylint: disable=no-member
    unknown: dict[str, t.Any] = {}
    previous_sorted_unknown = []
    while True:
        # The wire format frames protobuf messages with 0xc0, so the first `readuntil`
        # will return just that byte and the next will return the message with the
        # terminating byte.
        raw_buf = await reader.readuntil(b"\xc0")
        if len(raw_buf) == 1:
            continue
        buf = wireutils.remove_emulation_prevention(raw_buf[:-1])
        if dump:
            with open(f"dump-query-{i}.bin", "wb") as writer:
                writer.write(buf)
            i += 1
        root = aiobafi6_pb2.Root()  # pylint: disable=no-member
        root.ParseFromString(buf)
        for prop in root.root2.query_result.properties:
            for field in prop.UnknownFields():  # type: ignore
                unknown[str(field.field_number)] = field.data
            for field in prop.capabilities.UnknownFields():  # type: ignore
                unknown["capabilities." + str(field.field_number)] = field.data
            for field in prop.stats.UnknownFields():  # type: ignore
                unknown["stats." + str(field.field_number)] = field.data
        root.DiscardUnknownFields()  # type: ignore
        for prop in root.root2.query_result.properties:
            prop.ClearField("local_datetime")
            prop.ClearField("utc_datetime")
            prop.stats.ClearField("uptime_minutes")
            latest.MergeFrom(prop)
        diff = "".join(
            difflib.unified_diff(
                text_format.MessageToString(previous).splitlines(keepends=True),
                text_format.MessageToString(latest).splitlines(keepends=True),
            )
        )
        if len(diff) > 0:
            print(diff)
        previous.CopyFrom(latest)
        sorted_unknown = [f"{k}: {str(unknown[k])}\n" for k in sorted(unknown.keys())]
        diff = "".join(difflib.unified_diff(previous_sorted_unknown, sorted_unknown))
        if len(diff) > 0:
            print(diff)
        previous_sorted_unknown = sorted_unknown


def print_device(dev: Device):
    """Print a Device."""
    print("<-- callback")
    print(dev.properties_proto)
    print("callback -->")


async def query_state(ip_addr: str, interval: int):
    """Query state via a Device."""
    print(f"querying all state from {ip_addr}: interval {interval}s")
    dev = Device(
        Service(ip_addresses=[ip_addr], port=PORT), query_interval_seconds=interval
    )
    dev.add_callback(print_device)
    await dev.async_run()


async def direct_set_property(ip_addr: str, prop: str, value: int, dump: bool):
    """Set a property directly."""
    print(f"directly setting {prop} of {ip_addr} to {value}")
    root = aiobafi6_pb2.Root()  # pylint: disable=no-member
    try:
        setattr(root.root2.commit.properties, prop, value)
    except TypeError:
        setattr(root.root2.commit.properties, prop, int(value))
    buf = wireutils.serialize(root)
    if dump:
        with open(f"dump-set-{prop}-{value}.bin", "wb") as writer:
            writer.write(buf)
    _, writer = await asyncio.open_connection(ip_addr, PORT)
    writer.write(buf)
    await writer.drain()
    writer.close()
    await writer.wait_closed()


async def set_property(ip_addr: str, prop: str, value: int):
    """Set a property via a Device."""
    print(f"setting {prop} of {ip_addr} to {value}")
    dev = Device(Service(ip_addresses=[ip_addr], port=PORT), query_interval_seconds=0)
    dev.async_run()
    await dev.async_wait_available()
    try:
        setattr(dev, prop, value)
    except TypeError:
        setattr(dev, prop, int(value))


async def discover():
    """async discover"""
    aiozc = AsyncZeroconf(ip_version=IPVersion.V4Only)
    _ = ServiceBrowser(
        aiozc.zeroconf, lambda services: print(f"== Discover ==\n{services}")
    )
    while True:
        await asyncio.sleep(1)


async def async_main():
    """async_main"""
    args = ARGS.parse_args()
    logging.config.dictConfig(LOGGING)
    if args.discover:
        await discover()
        return
    if args.ip_addr is None:
        print("must provide an ip address")
        sys.exit(1)
    try:
        ip_addr = ipaddress.ip_address(args.ip_addr)
    except ValueError:
        print(f"invalid address: {args.ip_addr}")
        sys.exit(1)
    if args.property is not None:
        if args.value is None:
            raise RuntimeError("must specify property value")
        if args.direct:
            await direct_set_property(
                str(ip_addr), args.property, args.value, dump=args.dump
            )
        else:
            await set_property(str(ip_addr), args.property, args.value)
    elif args.direct:
        await direct_query_state(str(ip_addr), dump=args.dump, interval=args.interval)
    else:
        await query_state(str(ip_addr), interval=args.interval)


def main():
    """main"""
    try:
        asyncio.run(async_main())
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
