#!/usr/bin/env python3
#
# Copyright 2019 Ricardo Branco <rbranco@suse.de>
# MIT License
#
"""
Show all instances created on cloud providers
"""

import argparse
import datetime
import logging
import sys

from operator import attrgetter, itemgetter
from threading import Thread
from dateutil import parser

import jmespath
from jmespath.exceptions import JMESPathError

from cachetools import cached, TTLCache

from providers.amazon import AWS
from providers.az import Azure
from providers.gcp import GCP
from providers.exceptions import SomeError


USAGE = "Usage: " + sys.argv[0] + """ [OPTIONS]
Options:
    -a, --all               show all instances
    -o, --output text|html  output type
    -r, --reverse           reverse sort
    -s, --sort time         sort type
    -w, --web               run a web server on port 7777
    -h, --help              show this help message and exit
    -d, --debug             debug mode
    -V, --version           show version and exit
"""

__version__ = "0.1.2"

def pretty_date(date):
    """
    Converts datetime object or string to local time or the
    timezone specified by the TZ environment variable
    """
    date_fmt = "%a %b %d %H:%M:%S %Z %Y"
    # NOTE: From Python 3.3 we can omit tz in datetime.astimezone()
    if isinstance(date, datetime.datetime):
        return date.astimezone().strftime(date_fmt)
    if isinstance(date, str):
        return parser.parse(timestr=date).astimezone().strftime(date_fmt)
    if date is None:
        return ""
    return "%s" % date


KEYS = "cloud name type status created location"
FMT_STRING = "{d[cloud]}\t{d[name]:32}\t{d[type]:>23}\t{d[status]:>16}\t{d[created]:30}\t{d[location]}"

def print_header():
    """
    Print the header for output
    """
    if args.output == "text":
        print(FMT_STRING.format(d={
            _: _.upper()
            for _ in KEYS.split()
            }))
    elif args.output == "html":
        header = "\n".join(["<th>%s</th>" % _.upper() for _ in KEYS.split()])
        print('''<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Instances</title></head>
<style>
table, th, td {
  border: 1px solid black;
  border-collapse: collapse
}
th, td {
  padding: 10px;
}
th {
  text-align: left;
}
table#instances tr:nth-child(even) {
  background-color: #eee;
}
table#instances tr:nth-child(odd) {
 background-color: #fff;
}
</style>
<body><table style="width:77%" id="instances">
''' + header)

def print_info(**kwargs):
    """
    Print information about the instances
    """
    if args.output == "text":
        print(FMT_STRING.format(d=dict(kwargs)))
    elif args.output == "html":
        print(
            "<tr>\n" +
            "\n".join(["<td>%s</td>" % kwargs[_] for _ in KEYS.split()]) +
            "</tr>")


def print_footer():
    """
    Print the footer for output
    """
    if args.output == "html":
        print('</table></body></html>')


def perror(msg, err):
    """
    Print an error message and exit
    """
    logging.error("%s: %s", msg, err)
    sys.exit(1)


def print_amazon_instances():
    """
    Print information about AWS EC2 instances
    """
    filters = [
        {
            'Name': 'instance-state-name',
            'Values': [
                'pending', 'running', 'stopping',
                'stopped', 'shutting-down', 'terminated'
            ]
        }
    ]
    if not args.all:
        filters = [{'Name': 'instance-state-name', 'Values': ['running']}]
    try:
        instances = aws.get_instances(filters=filters)
    except SomeError as error:
        perror("AWS", error)
    if args.sort == "time":
        try:
            instances = [_ for _ in instances]
        except SomeError as error:
            perror("AWS", error)
        instances.sort(key=itemgetter('LaunchTime'), reverse=args.reverse)
    for instance in instances:
        print_info(
            cloud="AWS",
            name=instance['InstanceId'],
            type=instance['InstanceType'],
            status=aws.get_status(instance),
            created=pretty_date(instance['LaunchTime']),
            location=instance['Placement']['AvailabilityZone'])


def print_azure_instances():
    """
    Print information about Azure Compute instances
    """
    filter_instance_view = """
        statuses[1].display_status == 'VM starting' ||
        statuses[1].display_status == 'VM running' ||
        statuses[1].display_status == 'VM stopping' ||
        statuses[1].display_status == 'VM stopped' ||
        statuses[1].display_status == 'VM deallocating' ||
        statuses[1].display_status == 'VM deallocated'"""
    if not args.all:
        filter_instance_view = "statuses[1].display_status == 'VM running'"
    try:
        filter_instance_view = jmespath.compile(filter_instance_view)
    except JMESPathError as error:
        perror("Azure", error)
    try:
        instances = azure.get_instances(
            filter_instance_view=filter_instance_view)
    except SomeError as error:
        perror("Azure", error)
    if args.sort == "time":
        try:
            instances = [_ for _ in instances]
        except SomeError as error:
            perror("Azure", error)
        instances.sort(key=attrgetter('date'), reverse=args.reverse)
    for instance in instances:
        print_info(
            cloud="Azure",
            name=instance.name,
            type=instance.hardware_profile.vm_size,
            status=instance.status,
            created=pretty_date(instance.date),
            location=instance.location)


def print_google_instances():
    """
    Print information about Google Compute instances
    """
    filters = """
        status = provisioning OR
        status = staging OR
        status = running OR
        status = stopping OR
        status = stopped OR
        status = suspending OR
        status = suspended OR
        status = terminated
    """
    if not args.all:
        filters = "status = running"
    try:
        instances = gcp.get_instances(filters=filters)
    except SomeError as error:
        perror("GCP", error)
    if args.sort == "time":
        try:
            instances = [_ for _ in instances]
        except SomeError as error:
            perror("GCP", error)
        instances.sort(key=itemgetter('creationTimestamp'), reverse=args.reverse)
    for instance in instances:
        print_info(
            cloud="GCP",
            name=instance['name'],
            type=instance['machineType'].rsplit('/', 1)[-1],
            status=gcp.get_status(instance),
            created=pretty_date(instance['creationTimestamp']),
            location=instance['zone'].rsplit('/', 1)[-1])


@cached(cache=TTLCache(maxsize=2, ttl=120))
def main():
    """
    Main function
    """
    if args.web:
        sys.stdout = StringIO()
    print_header()
    threads = [
        Thread(target=print_amazon_instances),
        Thread(target=print_azure_instances),
        Thread(target=print_google_instances)
    ]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    print_footer()
    if args.web:
        sys.stdout.flush()
        response = sys.stdout.getvalue()
        sys.stdout.close()
        return response
    return None


def handle_requests(request):
    """
    Handle HTTP requests
    """
    logging.info(request)
    response = main()
    return Response(response)


def web_server():
    """
    Setup the WSGI server
    """
    with Configurator() as config:
        config.add_route('handle_requests', '/')
        config.add_view(handle_requests, route_name='handle_requests')
        app = config.make_wsgi_app()
        server = make_server('0.0.0.0', 7777, app)
        server.serve_forever()


def setup_logging():
    """
    Setup logging
    """
    logging.basicConfig(
        format="%(asctime)s %(levelname)-8s %(message)s" if args.web else None,
        stream=sys.stderr,
        level=logging.DEBUG if args.debug else \
            logging.INFO if args.web else logging.ERROR)


if __name__ == "__main__":
    argparser = argparse.ArgumentParser(usage=USAGE, add_help=False)
    argparser.add_argument('-a', '--all', action='store_true')
    argparser.add_argument('-d', '--debug', action='store_true')
    argparser.add_argument('-h', '--help', action='store_true')
    argparser.add_argument('-o', '--output', choices=['text', 'html'],
                           default="text")
    argparser.add_argument('-r', '--reverse', action='store_true')
    argparser.add_argument('-s', '--sort', choices=['time'])
    argparser.add_argument('-w', '--web', action='store_true')
    argparser.add_argument('-V', '--version', action='store_true')
    args = argparser.parse_args()

    if args.help or args.version:
        print(USAGE if args.help else __version__)
        sys.exit(0)

    setup_logging()

    try:
        aws = AWS()
    except SomeError as error:
        perror("AWS", error)
    try:
        azure = Azure()
    except SomeError as error:
        perror("Azure", error)
    try:
        gcp = GCP()
    except SomeError as error:
        perror("GCP", error)

    if args.web:
        from io import StringIO
        from wsgiref.simple_server import make_server
        from pyramid.config import Configurator
        from pyramid.response import Response

        args.output = "html"
        web_server()
        sys.exit(1)

    try:
        main()
    except KeyboardInterrupt:
        sys.exit(1)
