##############################################################################
# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Apache License, Version 2.0
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################


from django.template.loader import get_template
from django.http import HttpResponse
from django.utils import timezone

import yaml
import requests

from workflow.forms import ConfirmationForm
from api.models import JobFactory
from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
from resource_inventory.models import Image, GenericInterface, OPNFVConfig, HostOPNFVConfig, NetworkRole
from resource_inventory.resource_manager import ResourceManager
from resource_inventory.pdf_templater import PDFTemplater
from notifier.manager import NotificationHandler
from booking.models import Booking


class BookingAuthManager():
    """
    Verifies Booking Authorization.

    Class to verify that the user is allowed to book the requested resource
    The user must input a url to the INFO.yaml file to prove that they are the ptl of
    an approved project if they are booking a multi-node pod.
    This class parses the url and checks the logged in user against the info file.
    """

    LFN_PROJECTS = ["opnfv"]  # TODO

    def parse_github_url(self, url):
        project_leads = []
        try:
            parts = url.split("/")
            if "http" in parts[0]:  # the url include http(s)://
                parts = parts[2:]
            if parts[-1] != "INFO.yaml":
                return None
            if parts[0] not in ["github.com", "raw.githubusercontent.com"]:
                return None
            if parts[1] not in self.LFN_PROJECTS:
                return None
            # now to download and parse file
            if parts[3] == "blob":
                parts[3] = "raw"
            url = "https://" + "/".join(parts)
            info_file = requests.get(url, timeout=15).text
            info_parsed = yaml.load(info_file)
            ptl = info_parsed.get('project_lead')
            if ptl:
                project_leads.append(ptl)
            sub_ptl = info_parsed.get("subproject_lead")
            if sub_ptl:
                project_leads.append(sub_ptl)

        except Exception:
            pass

        return project_leads

    def parse_gerrit_url(self, url):
        project_leads = []
        try:
            halfs = url.split("?")
            parts = halfs[0].split("/")
            args = halfs[1].split(";")
            if "http" in parts[0]:  # the url include http(s)://
                parts = parts[2:]
            if "f=INFO.yaml" not in args:
                return None
            if "gerrit.opnfv.org" not in parts[0]:
                return None
            try:
                i = args.index("a=blob")
                args[i] = "a=blob_plain"
            except ValueError:
                pass
            # recreate url
            halfs[1] = ";".join(args)
            halfs[0] = "/".join(parts)
            # now to download and parse file
            url = "https://" + "?".join(halfs)
            info_file = requests.get(url, timeout=15).text
            info_parsed = yaml.load(info_file)
            ptl = info_parsed.get('project_lead')
            if ptl:
                project_leads.append(ptl)
            sub_ptl = info_parsed.get("subproject_lead")
            if sub_ptl:
                project_leads.append(sub_ptl)

        except Exception:
            return None

        return project_leads

    def parse_opnfv_git_url(self, url):
        project_leads = []
        try:
            parts = url.split("/")
            if "http" in parts[0]:  # the url include http(s)://
                parts = parts[2:]
            if "INFO.yaml" not in parts[-1]:
                return None
            if "git.opnfv.org" not in parts[0]:
                return None
            if parts[-2] == "tree":
                parts[-2] = "plain"
            # now to download and parse file
            url = "https://" + "/".join(parts)
            info_file = requests.get(url, timeout=15).text
            info_parsed = yaml.load(info_file)
            ptl = info_parsed.get('project_lead')
            if ptl:
                project_leads.append(ptl)
            sub_ptl = info_parsed.get("subproject_lead")
            if sub_ptl:
                project_leads.append(sub_ptl)

        except Exception:
            return None

        return project_leads

    def parse_url(self, info_url):
        """
        Parse the project URL.

        Gets the INFO.yaml file from the project and returns the PTL info.
        """
        if "github" in info_url:
            return self.parse_github_url(info_url)

        if "gerrit.opnfv.org" in info_url:
            return self.parse_gerrit_url(info_url)

        if "git.opnfv.org" in info_url:
            return self.parse_opnfv_git_url(info_url)

    def booking_allowed(self, booking, repo):
        """
        Assert the current Booking Policy.

        This is the method that will have to change whenever the booking policy changes in the Infra
        group / LFN. This is a nice isolation of that administration crap
        currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL,
        which is checked using the provided info file
        """
        if booking.owner.userprofile.booking_privledge:
            return True  # admin override for this user
        if Booking.objects.filter(owner=booking.owner, end__gt=timezone.now()).count() >= 3:
            return False
        if len(booking.resource.template.getResources()) < 2:
            return True  # if they only have one server, we dont care
        if repo.BOOKING_INFO_FILE not in repo.el:
            return False  # INFO file not provided
        ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE))
        for ptl in ptl_info:
            if ptl['email'] == booking.owner.userprofile.email_addr:
                return True
        return False


class WorkflowStepStatus(object):
    """
    Poor man's enum for the status of a workflow step.

    The steps in a workflow are not completed (UNTOUCHED)
    or they have been completed correctly (VALID) or they were filled out
    incorrectly (INVALID)
    """

    UNTOUCHED = 0
    INVALID = 100
    VALID = 200


class WorkflowStep(object):
    template = 'bad_request.html'
    title = "Generic Step"
    description = "You were led here by mistake"
    short_title = "error"
    metastep = None
    # phasing out metastep:

    valid = WorkflowStepStatus.UNTOUCHED
    message = ""

    enabled = True

    def cleanup(self):
        raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete implemented cleanup() method")

    def enable(self):
        if not self.enabled:
            self.enabled = True

    def disable(self):
        if self.enabled:
            self.cleanup()
            self.enabled = False

    def set_invalid(self, message, code=WorkflowStepStatus.INVALID):
        self.valid = code
        self.message = message

    def set_valid(self, message, code=WorkflowStepStatus.VALID):
        self.valid = code
        self.message = message

    def to_json(self):
        return {
            'title': self.short_title,
            'enabled': self.enabled,
            'valid': self.valid,
            'message': self.message,
        }

    def __init__(self, id, repo=None):
        self.repo = repo
        self.id = id

    def get_context(self):
        context = {}
        context['step_number'] = self.repo_get('steps')
        context['active_step'] = self.repo_get('active_step')
        context['render_correct'] = "true"
        context['step_title'] = self.title
        context['description'] = self.description
        return context

    def render(self, request):
        return HttpResponse(self.render_to_string(request))

    def render_to_string(self, request):
        template = get_template(self.template)
        return template.render(self.get_context(), request)

    def post(self, post_content, user):
        raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete post() method")

    def validate(self, request):
        pass

    def repo_get(self, key, default=None):
        return self.repo.get(key, default, self.id)

    def repo_put(self, key, value):
        return self.repo.put(key, value, self.id)


"""
subclassing notes:
    subclasses have to define the following class attributes:
        self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo
        self.form: the form to be used
        alert_bundle_missing(): what message to display if a user does not select/selects an invalid object
        get_form_queryset(): generate a queryset to be used to filter available items for the field
        get_page_context(): return simple context such as page header and other info
"""


class AbstractSelectOrCreate(WorkflowStep):
    template = 'dashboard/genericselect.html'
    title = "Select a Bundle"
    short_title = "select"
    description = "Generic bundle selector step"

    select_repo_key = None
    form = None  # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm

    def alert_bundle_missing(self):  # override in subclasses to change message if field isn't filled out
        self.set_invalid("Please select a valid bundle")

    def post(self, post_data, user):
        form = self.form(post_data, queryset=self.get_form_queryset())
        if form.is_valid():
            bundle = form.get_validated_bundle()
            if not bundle:
                self.alert_bundle_missing()
                return
            self.repo_put(self.select_repo_key, bundle)
            self.put_confirm_info(bundle)
            self.set_valid("Step Completed")
        else:
            self.alert_bundle_missing()

    def get_context(self):
        default = []

        bundle = self.repo_get(self.select_repo_key, False)
        if bundle:
            default.append(bundle)

        form = self.form(queryset=self.get_form_queryset(), initial=default)

        context = {'form': form, **self.get_page_context()}
        context.update(super().get_context())

        return context

    def get_page_context():
        return {
            'select_type': 'generic',
            'select_type_title': 'Generic Bundle'
        }


class Confirmation_Step(WorkflowStep):
    template = 'workflow/confirm.html'
    title = "Confirm Changes"
    description = "Does this all look right?"

    short_title = "confirm"

    def get_context(self):
        context = super(Confirmation_Step, self).get_context()
        context['form'] = ConfirmationForm()
        context['confirmation_info'] = yaml.dump(
            self.repo_get(self.repo.CONFIRMATION),
            default_flow_style=False
        ).strip()

        if self.valid == WorkflowStepStatus.VALID:
            context["confirm_succeeded"] = "true"

        return context

    def flush_to_db(self):
        errors = self.repo.make_models()
        if errors:
            return errors

    def post(self, post_data, user):
        form = ConfirmationForm(post_data)
        if form.is_valid():
            data = form.cleaned_data['confirm']
            if data == "True":
                errors = self.flush_to_db()
                if errors:
                    self.set_invalid("ERROR OCCURRED: " + errors)
                else:
                    self.set_valid("Confirmed")

            elif data == "False":
                self.set_valid("Canceled")
            else:
                self.set_invalid("Bad Form Contents")

        else:
            self.set_invalid("Bad Form Contents")


class Repository():

    EDIT = "editing"
    MODELS = "models"
    RESOURCE_SELECT = "resource_select"
    CONFIRMATION = "confirmation"
    SELECTED_GRESOURCE_BUNDLE = "selected generic bundle pk"
    SELECTED_CONFIG_BUNDLE = "selected config bundle pk"
    SELECTED_OPNFV_CONFIG = "selected opnfv deployment config"
    GRESOURCE_BUNDLE_MODELS = "generic_resource_bundle_models"
    GRESOURCE_BUNDLE_INFO = "generic_resource_bundle_info"
    BOOKING = "booking"
    LAB = "lab"
    GRB_LAST_HOSTLIST = "grb_network_previous_hostlist"
    BOOKING_FORMS = "booking_forms"
    SWCONF_HOSTS = "swconf_hosts"
    BOOKING_MODELS = "booking models"
    CONFIG_MODELS = "configuration bundle models"
    OPNFV_MODELS = "opnfv configuration models"
    SESSION_USER = "session owner user account"
    SESSION_MANAGER = "session manager for current session"
    VALIDATED_MODEL_GRB = "valid grb config model instance in db"
    VALIDATED_MODEL_CONFIG = "valid config model instance in db"
    VALIDATED_MODEL_BOOKING = "valid booking model instance in db"
    VLANS = "a list of vlans"
    SNAPSHOT_MODELS = "the models for snapshotting"
    SNAPSHOT_BOOKING_ID = "the booking id for snapshotting"
    SNAPSHOT_NAME = "the name of the snapshot"
    SNAPSHOT_DESC = "description of the snapshot"
    BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking"

    # migratory elements of segmented workflow
    # each of these is the end result of a different workflow.
    HAS_RESULT = "whether or not workflow has a result"
    RESULT_KEY = "key for target index that result will be put into in parent"
    RESULT = "result object from workflow"

    def get_child_defaults(self):
        return_tuples = []
        for key in [self.SELECTED_GRESOURCE_BUNDLE, self.SESSION_USER]:
            return_tuples.append((key, self.el.get(key)))
        return return_tuples

    def set_defaults(self, defaults):
        for key, value in defaults:
            self.el[key] = value

    def get(self, key, default, id):

        self.add_get_history(key, id)
        return self.el.get(key, default)

    def put(self, key, val, id):
        self.add_put_history(key, id)
        self.el[key] = val

    def add_get_history(self, key, id):
        self.add_history(key, id, self.get_history)

    def add_put_history(self, key, id):
        self.add_history(key, id, self.put_history)

    def add_history(self, key, id, history):
        if key not in history:
            history[key] = [id]
        else:
            history[key].append(id)

    def make_models(self):
        if self.SNAPSHOT_MODELS in self.el:
            errors = self.make_snapshot()
            if errors:
                return errors

        # if GRB WF, create it
        if self.GRESOURCE_BUNDLE_MODELS in self.el:
            errors = self.make_generic_resource_bundle()
            if errors:
                return errors
            else:
                self.el[self.HAS_RESULT] = True
                self.el[self.RESULT_KEY] = self.SELECTED_GRESOURCE_BUNDLE
                return

        if self.CONFIG_MODELS in self.el:
            errors = self.make_software_config_bundle()
            if errors:
                return errors
            else:
                self.el[self.HAS_RESULT] = True
                self.el[self.RESULT_KEY] = self.SELECTED_CONFIG_BUNDLE
                return

        if self.OPNFV_MODELS in self.el:
            errors = self.make_opnfv_config()
            if errors:
                return errors
            else:
                self.el[self.HAS_RESULT] = True
                self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG

        if self.BOOKING_MODELS in self.el:
            errors = self.make_booking()
            if errors:
                return errors
            # create notification
            booking = self.el[self.BOOKING_MODELS]['booking']
            NotificationHandler.notify_new_booking(booking)

    def make_snapshot(self):
        owner = self.el[self.SESSION_USER]
        models = self.el[self.SNAPSHOT_MODELS]
        image = models.get('snapshot', Image())
        booking_id = self.el.get(self.SNAPSHOT_BOOKING_ID)
        if not booking_id:
            return "SNAP, No booking ID provided"
        booking = Booking.objects.get(pk=booking_id)
        if booking.start > timezone.now() or booking.end < timezone.now():
            return "Booking is not active"
        name = self.el.get(self.SNAPSHOT_NAME)
        if not name:
            return "SNAP, no name provided"
        host = models.get('host')
        if not host:
            return "SNAP, no host provided"
        description = self.el.get(self.SNAPSHOT_DESC, "")
        image.from_lab = booking.lab
        image.name = name
        image.description = description
        image.public = False
        image.lab_id = -1
        image.owner = owner
        image.host_type = host.profile
        image.save()
        try:
            current_image = host.config.image
            image.os = current_image.os
            image.save()
        except Exception:
            pass
        JobFactory.makeSnapshotTask(image, booking, host)

        self.el[self.RESULT] = image
        self.el[self.HAS_RESULT] = True

    def make_generic_resource_bundle(self):
        owner = self.el[self.SESSION_USER]
        if self.GRESOURCE_BUNDLE_MODELS in self.el:
            models = self.el[self.GRESOURCE_BUNDLE_MODELS]
            if 'hosts' in models:
                hosts = models['hosts']
            else:
                return "GRB has no hosts. CODE:0x0002"
            if 'bundle' in models:
                bundle = models['bundle']
            else:
                return "GRB, no bundle in models. CODE:0x0003"

            try:
                bundle.owner = owner
                bundle.save()
            except Exception as e:
                return "GRB, saving bundle generated exception: " + str(e) + " CODE:0x0004"
            try:
                for host in hosts:
                    genericresource = host.resource
                    genericresource.bundle = bundle
                    genericresource.save()
                    host.resource = genericresource
                    host.save()
            except Exception as e:
                return "GRB, saving hosts generated exception: " + str(e) + " CODE:0x0005"

            if 'networks' in models:
                for net in models['networks'].values():
                    net.bundle = bundle
                    net.save()

            if 'interfaces' in models:
                for interface_set in models['interfaces'].values():
                    for interface in interface_set:
                        try:
                            interface.host = interface.host
                            interface.save()
                        except Exception:
                            return "GRB, saving interface " + str(interface) + " failed. CODE:0x0019"
            else:
                return "GRB, no interface set provided. CODE:0x001a"

            if 'connections' in models:
                for resource_name, mapping in models['connections'].items():
                    for profile_name, connection_set in mapping.items():
                        interface = GenericInterface.objects.get(
                            profile__name=profile_name,
                            host__resource__name=resource_name,
                            host__resource__bundle=models['bundle']
                        )
                        for connection in connection_set:
                            try:
                                connection.network = connection.network
                                connection.save()
                                interface.connections.add(connection)
                            except Exception as e:
                                return "GRB, saving vlan " + str(connection) + " failed. Exception: " + str(e) + ". CODE:0x0017"
            else:
                return "GRB, no vlan set provided. CODE:0x0018"

        else:
            return "GRB no models given. CODE:0x0001"

        self.el[self.RESULT] = bundle
        self.el[self.HAS_RESULT] = True
        return False

    def make_software_config_bundle(self):
        models = self.el[self.CONFIG_MODELS]
        if 'bundle' in models:
            bundle = models['bundle']
            bundle.bundle = self.el[self.SELECTED_GRESOURCE_BUNDLE]
            try:
                bundle.save()
            except Exception as e:
                return "SWC, saving bundle generated exception: " + str(e) + "CODE:0x0007"

        else:
            return "SWC, no bundle in models. CODE:0x0006"
        if 'host_configs' in models:
            host_configs = models['host_configs']
            for host_config in host_configs:
                host_config.bundle = host_config.bundle
                host_config.host = host_config.host
                try:
                    host_config.save()
                except Exception as e:
                    return "SWC, saving host configs generated exception: " + str(e) + "CODE:0x0009"
        else:
            return "SWC, no host configs in models. CODE:0x0008"
        if 'opnfv' in models:
            opnfvconfig = models['opnfv']
            opnfvconfig.bundle = opnfvconfig.bundle
            if opnfvconfig.scenario not in opnfvconfig.installer.sup_scenarios.all():
                return "SWC, scenario not supported by installer. CODE:0x000d"
            try:
                opnfvconfig.save()
            except Exception as e:
                return "SWC, saving opnfv config generated exception: " + str(e) + "CODE:0x000b"
        else:
            pass

        self.el[self.RESULT] = bundle
        return False

    def make_booking(self):
        models = self.el[self.BOOKING_MODELS]
        owner = self.el[self.SESSION_USER]

        if 'booking' in models:
            booking = models['booking']
        else:
            return "BOOK, no booking model exists. CODE:0x000f"

        selected_grb = None

        if self.SELECTED_GRESOURCE_BUNDLE in self.el:
            selected_grb = self.el[self.SELECTED_GRESOURCE_BUNDLE]
        else:
            return "BOOK, no selected resource. CODE:0x000e"

        if self.SELECTED_CONFIG_BUNDLE not in self.el:
            return "BOOK, no selected config bundle. CODE:0x001f"

        booking.config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]

        if not booking.start:
            return "BOOK, booking has no start. CODE:0x0010"
        if not booking.end:
            return "BOOK, booking has no end. CODE:0x0011"
        if booking.end <= booking.start:
            return "BOOK, end before/same time as start. CODE:0x0012"

        if 'collaborators' in models:
            collaborators = models['collaborators']
        else:
            return "BOOK, collaborators not defined. CODE:0x0013"
        try:
            resource_bundle = ResourceManager.getInstance().convertResourceBundle(selected_grb, config=booking.config_bundle)
        except ResourceAvailabilityException as e:
            return "BOOK, requested resources are not available. Exception: " + str(e) + " CODE:0x0014"
        except ModelValidationException as e:
            return "Error encountered when saving bundle. " + str(e) + " CODE: 0x001b"

        booking.resource = resource_bundle
        booking.owner = owner
        booking.lab = selected_grb.lab

        is_allowed = BookingAuthManager().booking_allowed(booking, self)
        if not is_allowed:
            return "BOOK, you are not allowed to book the requested resources"

        try:
            booking.save()
        except Exception as e:
            return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0015"

        for collaborator in collaborators:
            booking.collaborators.add(collaborator)

        try:
            booking.pdf = PDFTemplater.makePDF(booking)
            booking.save()
        except Exception as e:
            return "BOOK, failed to create Pod Desriptor File: " + str(e)

        try:
            JobFactory.makeCompleteJob(booking)
        except Exception as e:
            return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF"

        try:
            booking.save()
        except Exception as e:
            return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016"

        self.el[self.RESULT] = booking
        self.el[self.HAS_RESULT] = True

    def make_opnfv_config(self):
        opnfv_models = self.el[self.OPNFV_MODELS]
        config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE]
        if not config_bundle:
            return "No Configuration bundle selected"
        info = opnfv_models.get("meta", {})
        name = info.get("name", False)
        desc = info.get("description", False)
        if not (name and desc):
            return "No name or description given"
        installer = opnfv_models['installer_chosen']
        if not installer:
            return "No OPNFV Installer chosen"
        scenario = opnfv_models['scenario_chosen']
        if not scenario:
            return "No OPNFV Scenario chosen"

        opnfv_config = OPNFVConfig.objects.create(
            bundle=config_bundle,
            name=name,
            description=desc,
            installer=installer,
            scenario=scenario
        )

        network_roles = opnfv_models['network_roles']
        for net_role in network_roles:
            opnfv_config.networks.add(
                NetworkRole.objects.create(
                    name=net_role['role'],
                    network=net_role['network']
                )
            )

        host_roles = opnfv_models['host_roles']
        for host_role in host_roles:
            config = config_bundle.hostConfigurations.get(
                host__resource__name=host_role['host_name']
            )
            HostOPNFVConfig.objects.create(
                role=host_role['role'],
                host_config=config,
                opnfv_config=opnfv_config
            )

        self.el[self.RESULT] = opnfv_config
        self.el[self.HAS_RESULT] = True

    def __init__(self):
        self.el = {}
        self.el[self.CONFIRMATION] = {}
        self.el["active_step"] = 0
        self.el[self.HAS_RESULT] = False
        self.el[self.RESULT] = None
        self.get_history = {}
        self.put_history = {}