import argparse
import csv
from dataclasses import asdict
import datetime
from pathlib import Path
import sys
import requests
from time import sleep

from bleak.exc import BleakDeviceNotFoundError
from aranet4 import client


def parse_args(ctl_args):
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "device_mac",
        nargs="?",
        help="Aranet Bluetooth Address"
    )
    parser.add_argument(
        "--scan",
        action="store_true",
        help="Scan for Aranet devices"
    )

    current = parser.add_argument_group("Options for current reading")
    current.add_argument(
        "-u",
        "--url",
        metavar="URL",
        help="Remote url for current value push"
    )

    parser.add_argument(
        "-r",
        "--records",
        action="store_true",
        help="Fetch historical log records"
    )

    history = parser.add_argument_group("Filter History Log Records")
    history.add_argument(
        "-s",
        "--start",
        metavar="DATE",
        type=datetime.datetime.fromisoformat,
        help="Records range start (UTC time, example: 2019-09-29T14:00:00",
    )
    history.add_argument(
        "-e",
        "--end",
        metavar="DATE",
        type=datetime.datetime.fromisoformat,
        help="Records range end (UTC time, example: 2019-09-30T14:00:00",
    )
    history.add_argument(
        "-o",
        "--output",
        metavar="FILE",
        type=Path,
        help="Save records to a file"
    )
    history.add_argument(
        "-w",
        "--wait",
        action="store_true",
        default=False,
        help="Wait until new data point available",
    )
    history.add_argument(
        "-l",
        "--last",
        metavar="COUNT",
        type=int,
        help="Get <COUNT> last records"
    )
    history.add_argument(
        "--xt",
        dest="temp",
        default=True,
        action="store_false",
        help="Don't get temperature records",
    )
    history.add_argument(
        "--xh",
        dest="humi",
        default=True,
        action="store_false",
        help="Don't get humidity records",
    )
    history.add_argument(
        "--xp",
        dest="pres",
        default=True,
        action="store_false",
        help="Don't get pressure records",
    )
    history.add_argument(
        "--xc",
        dest="co2",
        default=True,
        action="store_false",
        help="Don't get co2 records",
    )
    settings = parser.add_argument_group("Change device settings")
    settings.add_argument(
        "--set-interval",
        dest="set_interval",
        metavar="MINUTES",
        type=int,
        help="Change update interval"
    )
    settings.add_argument(
        "--set-integrations",
        dest="set_integrations",
        type=str,
        choices=["on", "off"],
        help="Toggle Smart Home Integrations"
    )
    settings.add_argument(
        "--set-range",
        dest="set_btrange",
        type=str,
        choices=["normal", "extended"],
        help="Change bluetooth range"
    )

    return parser.parse_args(ctl_args)


def print_records(records):
    """Format log records to be printed to screen"""
    char_repeat = 34
    if records.filter.incl_co2:
        char_repeat += 9
    if records.filter.incl_temperature:
        char_repeat += 7
    if records.filter.incl_humidity:
        char_repeat += 8
    if records.filter.incl_pressure:
        char_repeat += 11
    if records.filter.incl_rad_dose_rate:
        char_repeat += 11
    if records.filter.incl_rad_dose:
        char_repeat += 11
    if records.filter.incl_rad_dose_total:
        char_repeat += 12
    if records.filter.incl_radon_concentration:
        char_repeat += 8
    print("-" * char_repeat)
    print(f"{'Device Name':<15}: {records.name:>20}")
    print(f"{'Device Version':<15}: {records.version:>20}")
    print("-" * char_repeat)
    print(f"{'id': ^4} | {'date': ^25} |", end="")
    if records.filter.incl_co2:
        print(f" {'co2':^6} |", end="")
    if records.filter.incl_temperature:
        print(" temp |", end="")
    if records.filter.incl_humidity:
        print(" humid |", end="")
    if records.filter.incl_pressure:
        print(" pressure |", end="")
    if records.filter.incl_rad_dose:
        print(" rad_dose |", end="")
    if records.filter.incl_rad_dose_rate:
        print(" rad_rate |", end="")
    if records.filter.incl_rad_dose_total:
        print(" rad_total |", end="")
    if records.filter.incl_radon_concentration:
        print(f" {'radon':^5} |", end="")
    print("")
    print("-" * char_repeat)

    for record_id, line in enumerate(
        records.value, start=records.filter.begin
    ):
        print(f"{record_id:>4d} | {line.date.isoformat()} |", end="")
        if records.filter.incl_co2:
            print(f" {line.co2:>6d} |", end="")
        if records.filter.incl_temperature:
            print(f" {line.temperature:>4.1f} |", end="")
        if records.filter.incl_humidity:
            print(f" {line.humidity:>5.1f} |", end="")
        if records.filter.incl_pressure:
            print(f" {line.pressure:>8.1f} |", end="")
        if records.filter.incl_rad_dose:
            print(f" {line.rad_dose/1000:>8.3f} |", end="")
        if records.filter.incl_rad_dose_rate:
            print(f" {line.rad_dose_rate/1000:>8.3f} |", end="")
        if records.filter.incl_rad_dose_total:
            print(f" {line.rad_dose_total/1000000:>9.4f} |", end="")
        if records.filter.incl_radon_concentration:
            print(f" {line.radon_concentration:>5d} |", end="")
        print("")
    print("-" * char_repeat)


def store_and_print_scan_result(found, advertisement):
    if not advertisement.device:
        return

    if advertisement.device.address in found:
        return
    
    found[advertisement.device.address] = advertisement
    if advertisement.readings:
        print(advertisement.readings.toString(advertisement))
    else:
        print("=======================================")
        print(f"  Name:     {advertisement.device.name}")
        print(f"  Address:  {advertisement.device.address}")
        print(f"  RSSI:     {advertisement.rssi} dBm")
        print()


def write_csv(filename, log_data):
    """
    Output `client.Record` dataclass to csv file
    :param filename: file name
    :param log_data: `client.Record` data object
    """
    with open(
        file=filename, mode="w", encoding="utf-8", newline=""
    ) as csv_file:
        fieldnames = ["date"]
        if log_data.filter.incl_co2:
            fieldnames.append("co2")
        if log_data.filter.incl_temperature:
            fieldnames.append("temperature")
        if log_data.filter.incl_humidity:
            fieldnames.append("humidity")
        if log_data.filter.incl_pressure:
            fieldnames.append("pressure")
        if log_data.filter.incl_rad_dose:
            fieldnames.append("rad_dose")
        if log_data.filter.incl_rad_dose_rate:
            fieldnames.append("rad_dose_rate")
        if log_data.filter.incl_rad_dose_total:
            fieldnames.append("rad_dose_total")
        if log_data.filter.incl_radon_concentration:
            fieldnames.append("radon_concentration")
        writer = csv.DictWriter(
            csv_file, fieldnames=fieldnames, extrasaction="ignore"
        )

        writer.writeheader()

        for line in log_data.value:
            writer.writerow(asdict(line))


def post_data(url, current):
    # get current measurement minute
    now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
    delta_ago = datetime.timedelta(seconds=current.ago)
    t = now - delta_ago
    t = t.replace(second=0)  # epoch, floored to minutes
    data = current.toDict()
    data["time"] = t.timestamp()
    r = requests.post(
        url=url,
        data=data,
        timeout=300
    )
    print(f"Pushing data: {r.text}")


def wait_for_new_record(address):
    current_vals = client.get_current_readings(address)
    wait_time = current_vals.interval - current_vals.ago
    for secs in range(wait_time, 0, -1):
        sleep(1)
        print(f"Next data point in {secs}...", end="\r")


def main(argv):
    found = {}
    args = parse_args(argv)

    if args.scan:
        print("Looking for Aranet devices...")
        devices = client.find_nearby(lambda ad: store_and_print_scan_result(found, ad))
        print(f"Scan finished. Found {len(devices)}")
        return

    if not args.device_mac:
        print("Device address not specified")
        return

    try:
        if args.records:
            if args.wait:
                wait_for_new_record(args.device_mac)
            records = client.get_all_records(args.device_mac, vars(args), True)
            print_records(records)
            if args.output:
                write_csv(args.output, records)
        else:
            settings = {}

            if args.set_interval:
                settings["interval"] = args.set_interval

            if args.set_integrations:
                settings["integrations"] = args.set_integrations

            if args.set_btrange:
                settings["range"] = args.set_btrange

            if settings:
                result = client.set_settings(args.device_mac, settings, True)
                for k in result:
                    val = settings[k]
                    ret = "SUCCESS" if result[k] else "FAILED"
                    print(f"Set {k} to \"{val}\": {ret}")
            else:
                current = client.get_current_readings(args.device_mac)
                print(current.toString())
                if args.url:
                    post_data(args.url, current)
    except (client.Aranet4Error, BleakDeviceNotFoundError) as e:
        print(e)


def entry_point():
    main(argv=sys.argv[1:])


if __name__ == "__main__":
    entry_point()
