###############################################################################
# Copyright (c) 2017 Koren Lev (Cisco Systems), Yaron Yogev (Cisco Systems)   #
# 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                                  #
###############################################################################
import calendar
import re
import requests
import time

from discover.configuration import Configuration
from discover.fetcher import Fetcher
from utils.string_utils import jsonify


class ApiAccess(Fetcher):
    subject_token = None
    initialized = False
    regions = {}
    config = None
    api_config = None

    host = ""
    base_url = ""
    admin_token = ""
    tokens = {}
    admin_endpoint = ""
    admin_project = None
    auth_response = {}

    alternative_services = {
        "neutron": ["quantum"]
    }

    # identitity API v2 version with admin token
    def __init__(self):
        super(ApiAccess, self).__init__()
        if ApiAccess.initialized:
            return
        ApiAccess.config = Configuration()
        ApiAccess.api_config = ApiAccess.config.get("OpenStack")
        host = ApiAccess.api_config["host"]
        ApiAccess.host = host
        port = ApiAccess.api_config["port"]
        if not (host and port):
            raise ValueError('Missing definition of host or port ' +
                             'for OpenStack API access')
        ApiAccess.base_url = "http://" + host + ":" + port
        ApiAccess.admin_token = ApiAccess.api_config["admin_token"]
        ApiAccess.admin_project = ApiAccess.api_config["admin_project"] \
            if "admin_project" in ApiAccess.api_config \
            else 'admin'
        ApiAccess.admin_endpoint = "http://" + host + ":" + "35357"

        token = self.v2_auth_pwd(ApiAccess.admin_project)
        if not token:
            raise ValueError("Authentication failed. Failed to obtain token")
        else:
            self.subject_token = token

    @staticmethod
    def parse_time(time_str):
        try:
            time_struct = time.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ")
        except ValueError:
            try:
                time_struct = time.strptime(time_str,
                                            "%Y-%m-%dT%H:%M:%S.%fZ")
            except ValueError:
                return None
        return time_struct

    # try to use existing token, if it did not expire
    def get_existing_token(self, project_id):
        try:
            token_details = ApiAccess.tokens[project_id]
        except KeyError:
            return None
        token_expiry = token_details["expires"]
        token_expiry_time_struct = self.parse_time(token_expiry)
        if not token_expiry_time_struct:
            return None
        token_expiry_time = token_details["token_expiry_time"]
        now = time.time()
        if now > token_expiry_time:
            # token has expired
            ApiAccess.tokens.pop(project_id)
            return None
        return token_details

    def v2_auth(self, project_id, headers, post_body):
        subject_token = self.get_existing_token(project_id)
        if subject_token:
            return subject_token
        req_url = ApiAccess.base_url + "/v2.0/tokens"
        response = requests.post(req_url, json=post_body, headers=headers)
        response = response.json()
        ApiAccess.auth_response[project_id] = response
        if 'error' in response:
            e = response['error']
            self.log.error(str(e['code']) + ' ' + e['title'] + ': ' +
                           e['message'] + ", URL: " + req_url)
            return None
        try:
            token_details = response["access"]["token"]
        except KeyError:
            # assume authentication failed
            return None
        token_expiry = token_details["expires"]
        token_expiry_time_struct = self.parse_time(token_expiry)
        if not token_expiry_time_struct:
            return None
        token_expiry_time = calendar.timegm(token_expiry_time_struct)
        token_details["token_expiry_time"] = token_expiry_time
        ApiAccess.tokens[project_id] = token_details
        return token_details

    def v2_auth_pwd(self, project):
        user = ApiAccess.api_config["user"]
        pwd = ApiAccess.api_config["pwd"]
        post_body = {
            "auth": {
                "passwordCredentials": {
                    "username": user,
                    "password": pwd
                }
            }
        }
        if project is not None:
            post_body["auth"]["tenantName"] = project
            project_id = project
        else:
            project_id = ""
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json; charset=UTF-8'
        }
        return self.v2_auth(project_id, headers, post_body)

    @staticmethod
    def get_auth_response(project_id):
        auth_response = ApiAccess.auth_response.get(project_id)
        if not auth_response:
            auth_response = ApiAccess.auth_response.get('admin', {})
        return auth_response

    def get_rel_url(self, relative_url, headers):
        req_url = ApiAccess.base_url + relative_url
        return self.get_url(req_url, headers)

    def get_url(self, req_url, headers):
        response = requests.get(req_url, headers=headers)
        if response.status_code != requests.codes.ok:
            # some error happened
            if "reason" in response:
                msg = ", reason: {}".format(response.reason)
            else:
                msg = ", response: {}".format(response.text)
            self.log.error("req_url: {} {}".format(req_url, msg))
            return None
        ret = response.json()
        return ret

    def get_region_url(self, region_name, service):
        if region_name not in self.regions:
            return None
        region = self.regions[region_name]
        s = self.get_service_region_endpoints(region, service)
        if not s:
            return None
        orig_url = s["adminURL"]
        # replace host name with the host found in config
        url = re.sub(r"^([^/]+)//[^:]+", r"\1//" + ApiAccess.host, orig_url)
        return url

    # like get_region_url(), but remove everything starting from the "/v2"
    def get_region_url_nover(self, region, service):
        full_url = self.get_region_url(region, service)
        if not full_url:
            self.log.error("could not find region URL for region: " + region)
            exit()
        url = re.sub(r":([0-9]+)/v[2-9].*", r":\1", full_url)
        return url

    def get_catalog(self, pretty):
        return jsonify(self.regions, pretty)

    # find the endpoints for a given service name,
    # considering also alternative service names
    def get_service_region_endpoints(self, region, service):
        alternatives = [service]
        endpoints = region["endpoints"]
        if service in self.alternative_services:
            alternatives.extend(self.alternative_services[service])
        for sname in alternatives:
            if sname in endpoints:
                return endpoints[sname]
        return None