import argparse
import logging
import os
import sys

import numpy
from gpstime import GPSTimeParseAction, gpsnow

from . import Client, __version__, constants
from .block import time_as_ns
from .flight import flight
from .format import FORMAT_OPTIONS, formatter

logger = logging.getLogger("arrakis")


##########


parser = argparse.ArgumentParser()
parser.add_argument("--version", "-v", action="version", version=__version__)
parser.add_argument(
    "--url",
    "-u",
    type=str,
    help="initial server url",
)
subparsers = parser.add_subparsers()


def add_subparser(cmd, **kwargs):
    sp = subparsers.add_parser(
        cmd.__name__,
        help=cmd.__doc__.splitlines()[0],
        description=cmd.__doc__.strip(),
        formatter_class=argparse.RawDescriptionHelpFormatter,
        **kwargs,
    )
    sp.set_defaults(func=cmd)
    return sp


##########


def _add_find_count_args(parser):
    parser.add_argument(
        "pattern",
        type=str,
        nargs="?",
        default=constants.DEFAULT_MATCH,
        help="channel pattern",
    )
    parser.add_argument(
        "--data-type",
        "--dtype",
        metavar="DTYPE",
        type=numpy.dtype,
        # action="append",
        help="data type",
    )
    parser.add_argument(
        "--min_rate",
        metavar="INT",
        type=int,
        help="minimum sample rate",
    )
    parser.add_argument(
        "--max_rate",
        metavar="INT",
        type=int,
        help="maximum sample rate",
    )
    parser.add_argument(
        "--publisher",
        metavar="ID",
        type=str,
        # action="append",
        help="publisher ID",
    )


def _add_format_args(parser, *, abrv=False):
    egroup = parser.add_mutually_exclusive_group()
    for name, help_ in FORMAT_OPTIONS.items():
        egroup.add_argument(
            f"--{name}",
            action="store_const",
            dest="format",
            const=name,
            help=f"format output as {help_}",
        )
    egroup.set_defaults(format="str")


##################################################


def find(args):
    """find channels matching regexp pattern"""
    format_ = args.format
    del args.format
    client = Client(url=args.url)
    del args.url
    for chan in client.find(**vars(args)):
        print(formatter(chan, format=format_))


sparser = add_subparser(find, aliases=["search", "list"])
_add_find_count_args(sparser)
_add_format_args(sparser)


##########


def count(args):
    """count channels matching pattern"""
    client = Client(url=args.url)
    del args.url
    print(client.count(**vars(args)))


sparser = add_subparser(count)
_add_find_count_args(sparser)


##########


def describe(args):
    """describe channels"""
    format_ = args.format
    del args.format
    client = Client(url=args.url)
    del args.url
    for chan in client.describe(**vars(args)).values():
        print(formatter(chan, format=format_))


sparser = add_subparser(describe, aliases=["show"])
sparser.add_argument("channels", nargs="+", help="list of channels to describe")
_add_format_args(sparser)


##########


def stream(args):
    """stream data for channels"""
    channels = args.channels
    start = None
    end = None
    if args.start:
        start = args.start.gps()
    if args.end:
        end = args.end.gps()
    client = Client(url=args.url)
    next_time = None
    for i, block in enumerate(
        client.stream(
            channels,
            start=start,
            end=end,
            kafka_url=args.kafka_url,
        )
    ):
        print(formatter(block, format=args.format))
        if next_time and block.time != next_time:
            print("WARNING: SKIPPED BLOCK!", file=sys.stderr)
        if args.latency:
            latency = gpsnow() - block.time
            print(f"block {i} latency: {latency} s", file=sys.stderr)
        next_time = block.time + block.duration


sparser = add_subparser(stream)
sparser.add_argument("channels", nargs="+", help="list of channels to stream")
sparser.add_argument(
    "--kafka-url",
    type=str,
    help="kafka URL",
)
sparser.add_argument(
    "--start",
    action=GPSTimeParseAction,
    help="start time (GPS or arbitrary date/time string)",
)
sparser.add_argument(
    "--end",
    action=GPSTimeParseAction,
    help="end time (GPS or arbitrary date/time string)",
)
sparser.add_argument(
    "--latency",
    action="store_true",
    help="print buffer latency to stderr",
)
_add_format_args(sparser, abrv=True)


##########


def fetch(args):
    """fetch data for channels"""
    format_ = args.format
    del args.format
    args.start = args.start.gps()
    args.end = args.end.gps()
    client = Client(url=args.url)
    del args.url
    block = client.fetch(**vars(args))
    print(formatter(block, format=format_))


sparser = add_subparser(fetch)
sparser.add_argument("channels", nargs="+", help="list of channels to fetch")
sparser.add_argument(
    "--start",
    required=True,
    action=GPSTimeParseAction,
    help="start time (GPS or arbitrary date/time string)",
)
sparser.add_argument(
    "--end",
    required=True,
    action=GPSTimeParseAction,
    help="end time (GPS or arbitrary date/time string)",
)
_add_format_args(sparser, abrv=True)


##########


def publish(args):
    """publish values to channels

    Arguments should be channel name + generator function pairs.  The
    generator function is used to generate data for the specified
    channel and should be a sympy expression, with the 't' value used
    to indicate time, e.g. "sin(t)".  A numeric value for the
    generator function will generate a constant stream.

    """
    import sched
    import time

    from sympy import lambdify, parse_expr
    from sympy.abc import t

    from . import Publisher, SeriesBlock, Time

    if not args.list:
        if not args.channel_args or len(args.channel_args) % 2 != 0:
            parser.error("arguments must be channel name + generator function pairs")
        chan_funcs = {}
        for name, value in zip(args.channel_args[::2], args.channel_args[1::2]):
            expr = parse_expr(value)
            chan_funcs[name] = lambdify(t, expr, "numpy")

    publisher = Publisher(args.publisher_id, args.url)
    publisher.register()

    if args.list:
        for _name, channel in publisher.channels.items():
            print(formatter(channel, format="repr"))
        return

    assert publisher.stride
    stride = publisher.stride

    def _gen_data(publisher, time_ns):
        metadata = {}
        series = {}
        for name, func in chan_funcs.items():
            try:
                channel = publisher.channels[name]
            except KeyError:
                msg = f"unknown channel for publisher: {name}"
                raise ValueError(msg) from None
            samples = channel.sample_rate * stride / Time.SECONDS
            assert int(samples) == samples, f"non-integer samples: {samples}"
            # make time array for the previous block of time
            time_array = time_ns - numpy.arange(samples)
            data = numpy.array(
                numpy.broadcast_to(func(time_array), time_array.shape),
                dtype=channel.data_type,
            )
            series[name] = data
            metadata[name] = channel
        sblock = SeriesBlock(
            time_ns,
            series,
            metadata,
        )
        # logger.info("publish: %s", repr_block(sblock))
        publisher.publish(sblock)

    def gpsnow_ns():
        return time_as_ns(gpsnow())

    def sleep_ns(ns):
        s = ns / Time.SECONDS
        time.sleep(s)

    s = sched.scheduler(gpsnow_ns, sleep_ns)
    tick = int(gpsnow()) * Time.SECONDS
    with publisher:
        while True:
            tick += stride
            s.enterabs(tick, 0, _gen_data, (publisher, tick))
            s.run()


sparser = add_subparser(publish)
sparser.add_argument(
    "--publisher-id",
    "-p",
    type=str,
    required=True,
    help="publisher ID (required)",
)
lgroup = sparser.add_mutually_exclusive_group()
lgroup.add_argument(
    "--list",
    "-l",
    action="store_true",
    help="list publisher channels and exit",
)
lgroup.add_argument(
    "channel_args",
    nargs="*",
    metavar="CHANNEL FUNC",
    default=[],
    help="channel name + generator function pairs",
)


##################################################


def main():
    logger.setLevel(os.getenv("LOG_LEVEL", "DEBUG").upper())
    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
    logger.addHandler(handler)

    args = parser.parse_args()

    if "func" not in args:
        parser.print_help()
        return

    func = args.func
    del args.func
    logger.debug(args)

    try:
        func(args)
    except flight.FlightError as e:
        msg = f"request error: {e}"
        raise SystemExit(msg) from e


if __name__ == "__main__":
    import signal

    signal.signal(signal.SIGINT, signal.SIG_DFL)
    main()
