#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright © 2015, 2017, 2022 Collabora, Ltd.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
# Author: Daniel Stone <daniels@collabora.com>
# Author: Guillaume Tucker <guillaume.tucker@collabora.com>
# Author: Laura Nao <laura.nao@collabora.com>
#
# A wrapper around servod: start servod docker container and create
# any device links.

from configparser import ConfigParser, NoSectionError
from itertools import zip_longest
from systemd.daemon import notify
from xmlrpc import client

import logging
import os
import shutil
import pyudev
import sys
import csv
import docker
import signal


CFG_FIELDS = ['servo-name', 'serial-number',
              'port-number', 'board-name', 'board-model']

dev_dir = None
image = None


def get_config():
    config = ConfigParser()
    config.read('/etc/run-docker-servod.conf')

    return config

def setup_client():
    config = get_config()

    global image
    image = f"{config.get('image', 'url')}:{config.get('image', 'tag')}"

    client = docker.from_env()
    try:
        logging.info(f'Pulling: {image}')
        client.images.pull(image)
    except docker.errors.APIError:
        logging.exception(f'Failed to pull {image}')
        return None

    return client


def force_remove_container(client, cont_name):
    try:
        cont = client.containers.get(cont_name)
        cont.remove(force=True)
    except Exception:
        logging.info("Container %s is not running.", cont_name)
        return

    logging.warning("Prevented race condition for container %s", cont_name)


def start_servod_container(client, hostname, serial, port, board, model):
    environment = [
        f'BOARD={board}',
        f'MODEL={model}',
        f'SERIAL={serial}',
        f'PORT={9999}',
    ]

    cont_name = f'{hostname}-docker_servod'

    force_remove_container(client, cont_name)
    cont = client.containers.run(
        image,
        name=cont_name,
        hostname=cont_name,
        environment=environment,
        remove=True,
        privileged=True,
        detach=True,
        cap_add=["NET_ADMIN"],
        command=["bash", "/start_servod.sh"],
        # mount logs volume
        volumes=["/dev:/dev", f'{hostname}-log_servod:/var/log/servod_9999/'],
        # map 9999 container port to host port
        ports={'9999': port}
    )

    for log in cont.logs(stream=True, follow=True):
        if b'servod - INFO - Listening on 0.0.0.0 port' in log:
            logging.info("Servod has started.")
            break

    return cont


def stop_servod_container(cont):
    cont.stop()

    do_cleanup(0)


def handle_sigterm(signum, frame, cont):
    stop_servod_container(cont)


def do_connect(port=9999):
    remote_uri = f'http://0.0.0.0:{port}'
    servo_client = client.ServerProxy(remote_uri, verbose=False)

    for dev in ['ec', 'cpu', 'cr50']:
        console_name = f'{dev}_uart_pty'
        try:
            pts = servo_client.get(console_name)
        except client.Fault:
            continue

        pts_link = os.path.join(dev_dir, "-".join([dev, "uart"]))

        os.symlink(pts, pts_link)
        logging.info(f"{dev.upper()} UART: {pts} -> {pts_link}")


def do_cleanup(status):
    try:
        if dev_dir:
            shutil.rmtree(dev_dir, ignore_errors=True)
    except:
        logging.warning(f'Failed to remove {dev_dir}')

    logging.info("Servod container stopped.")

    sys.exit(status)


def run_servod():
    try:
        dev_path = sys.argv[1]
    except:
        logging.exception(f'Usage: {sys.argv[0]} device-path')
        sys.exit(1)

    ud_ctx = pyudev.Context()
    if not ud_ctx:
        logging.exception("Couldn't create udev context")
        sys.exit(1)

    logging.info(f'Device path: {dev_path}')

    try:
        ud_dev = pyudev.Devices.from_device_file(ud_ctx, dev_path)
    except:
        logging.exception(f"Couldn't find udev device from {dev_path}")
        sys.exit(1)

    if "serial" not in ud_dev.attributes.available_attributes:
        logging.error(
            f'Device {ud_dev.sys_path} has no serial attribute')
        sys.exit(1)

    target_serial = ud_dev.attributes.get("serial")
    logging.info(f'Board serial: {target_serial}')

    all_boards = []

    rcfile_path = "/etc/google-servo.conf"
    config = get_config()
    try:
        rcfile_path = config.get('servod-conf', 'path')
    except NoSectionError:
        pass

    # Parse configuration file; skip lines beginning with #,
    # ignore white spaces and empty lines
    with open(rcfile_path) as rcfile:
        conf = csv.reader(
            filter(lambda row: row[0] != '#', rcfile), skipinitialspace=True)
        for row in conf:
            if row:
                all_boards.append(dict(zip_longest(CFG_FIELDS, row, fillvalue = None)))

    board_conf = None

    for board in all_boards:
        if board['serial-number'] == target_serial.decode('ascii'):
            board_conf = board

    if not board_conf:
        logging.error(
            f"Couldn't get board name for serial {target_serial}")
        sys.exit(1)

    logging.info(f'Board configuration: {board_conf}')

    # Start servod docker container
    client = setup_client()
    servod_cont = start_servod_container(client, board_conf['servo-name'], board_conf['serial-number'],
                                         board_conf['port-number'], board_conf['board-name'], board_conf['board-model'])
    if not servod_cont:
        logging.error("Couldn't start servod container")
        sys.exit(1)

    global dev_dir
    dev_dir = os.path.join("/dev/google-servo/", board_conf['servo-name'])
    logging.info(f'Device directory: {dev_dir}')
    os.makedirs(dev_dir, exist_ok=True)

    # Create device symlinks
    do_connect(board_conf['port-number'])

    # Register sigterm handler
    signal.signal(signal.SIGTERM,
                  lambda signum, frame: handle_sigterm(signum, frame, servod_cont))

    notify("READY=1")
    notify(
        f'STATUS=Board {board_conf["servo-name"]} on port {board_conf["port-number"]}')

    ret = servod_cont.wait()

    do_cleanup(ret['StatusCode'])


if __name__ == '__main__':
    logging.getLogger().setLevel(logging.INFO)

    run_servod()
