import base64
import copy
import json
import kubernetes
import logging
import os
import queue
import socket
import threading
import time

from anarchyutil import deep_update

operator_logger = logging.getLogger('operator')

class AnarchyRuntime(object):
    def __init__(
        self,
        operator_domain=None,
        operator_namespace=None
    ):
        self.__init_domain(operator_domain)
        self.__init_namespace(operator_namespace)
        self.__init_kube_apis()
        self.is_active = False
        self.is_active_condition = threading.Condition()
        self.pod_name = os.environ.get('POD_NAME', os.environ.get('HOSTNAME', None))
        self.pod = self.core_v1_api.read_namespaced_pod(self.pod_name, self.operator_namespace)
        self.running_all_in_one = '' != os.environ.get('ODO_S2I_SCRIPTS_URL', '')
        if self.running_all_in_one:
            self.anarchy_service_name = os.environ.get('ANARCHY_SERVICE', socket.gethostbyname(os.environ.get('HOSTNAME')))
            self.anarchy_service = self.pod
        else:
            self.anarchy_service_name = os.environ.get('ANARCHY_SERVICE', 'anarchy')
            self.anarchy_service = self.core_v1_api.read_namespaced_service(self.anarchy_service_name, self.operator_namespace)
        self.__init_callback_base_url()
        self.anarchy_runners = {}
        self.last_lost_runner_check = time.time()
        self.api_version = 'v1'
        self.api_group_version = self.operator_domain + '/' + self.api_version
        self.action_label = self.operator_domain + '/action'
        self.event_label = self.operator_domain + '/event'
        self.finished_label = self.operator_domain + '/finished'
        self.governor_label = self.operator_domain + '/governor'
        self.run_label = self.operator_domain + '/run'
        self.runner_label = self.operator_domain + '/runner'
        self.runner_terminating_label = self.operator_domain + '/runner-terminating'
        self.subject_label = self.operator_domain + '/subject'
        # Detect if running in development environment and switch to single pod, all-in-one, mode

    def __init_domain(self, operator_domain):
        if operator_domain:
            self.operator_domain = operator_domain
        else:
            self.operator_domain = os.environ.get('ANARCHY_DOMAIN', 'anarchy.gpte.redhat.com')

    def __init_kube_apis(self):
        if os.path.exists('/run/secrets/kubernetes.io/serviceaccount/token'):
            f = open('/run/secrets/kubernetes.io/serviceaccount/token')
            kube_auth_token = f.read()
            kube_config = kubernetes.client.Configuration()
            kube_config.api_key['authorization'] = 'Bearer ' + kube_auth_token
            kube_config.host = os.environ['KUBERNETES_PORT'].replace('tcp://', 'https://', 1)
            kube_config.ssl_ca_cert = '/run/secrets/kubernetes.io/serviceaccount/ca.crt'
        else:
            kubernetes.config.load_kube_config()
            kube_config = None

        self.api_client = kubernetes.client.ApiClient(kube_config)
        self.core_v1_api = kubernetes.client.CoreV1Api(self.api_client)
        self.custom_objects_api = kubernetes.client.CustomObjectsApi(self.api_client)

    def __init_namespace(self, operator_namespace):
        if operator_namespace:
            self.operator_namespace = operator_namespace
        elif 'OPERATOR_NAMESPACE' in os.environ:
            self.operator_namespace = os.environ['OPERATOR_NAMESPACE']
        elif os.path.exists('/run/secrets/kubernetes.io/serviceaccount/namespace'):
            f = open('/run/secrets/kubernetes.io/serviceaccount/namespace')
            self.operator_namespace = f.read()
        else:
            raise Exception('Unable to determine operator namespace. Please set OPERATOR_NAMESPACE environment variable.')

    def __init_callback_base_url(self):
        url = os.environ.get('CALLBACK_BASE_URL', '')
        if url and len(url) > 8:
            self.callback_base_url = url
            return
        if self.running_all_in_one:
            self.callback_base_url = 'http://{}:5000'.format(self.anarchy_service_name)
            return
        try:
            route = self.custom_objects_api.get_namespaced_custom_object(
                'route.openshift.io', 'v1', self.operator_namespace, 'routes', self.anarchy_service_name
            )
            spec = route.get('spec', {})
            if spec.get('tls', None):
                self.callback_base_url = 'https://' + spec['host']
            else:
                self.callback_base_url = 'http://' + spec['host']
            operator_logger.info('Set callback base url from OpenShift route: %s', self.callback_base_url)
        except kubernetes.client.rest.ApiException as e:
            if e.status == 404:
                route = self.custom_objects_api.create_namespaced_custom_object(
                    'route.openshift.io', 'v1', self.operator_namespace, 'routes',
                    {
                        'apiVersion': 'route.openshift.io/v1',
                        'kind': 'Route',
                        'metadata': {
                            'name': self.anarchy_service_name,
                            'namespace': self.operator_namespace,
                            'ownerReferences': [{
                                'apiVersion': self.anarchy_service.api_version,
                                'controller': True,
                                'kind': self.anarchy_service.kind,
                                'name': self.anarchy_service.metadata.name,
                                'uid': self.anarchy_service.metadata.uid
                            }]
                        },
                        'spec': {
                            'port': { 'targetPort': 'api' },
                            'tls': { 'termination': 'edge' },
                            'to': {
                                'kind': 'Service',
                                'name': self.anarchy_service_name
                            }
                        }
                    }
                )
                self.callback_base_url = 'https://' + route['spec']['host']
                operator_logger.info('Created OpenShift route %s and set callback base url: %s', route['metadata']['name'], self.callback_base_url)
            else:
                operator_logger.warning('Unable to determine a callback url. Callbacks will not function.')
                self.callback_base_url = None

    def action_callback_url(self, action_name):
        if not self.callback_base_url:
            raise Exception('Unable to set action callback URL. Please set CALLBACK_BASE_URL environment variable.')
        return '{}/action/{}'.format(self.callback_base_url, action_name)

    def get_secret_data(self, secret_name, secret_namespace=None):
        if not secret_namespace:
            secret_namespace = self.operator_namespace
        secret = self.core_v1_api.read_namespaced_secret(
            secret_name, secret_namespace
        )
        data = { k: base64.b64decode(v).decode('utf-8') for (k, v) in secret.data.items() }

        # Attempt to evaluate secret data valuse as YAML
        for k, v in data.items():
            try:
                data[k] = json.loads(v)
            except json.decoder.JSONDecodeError:
                pass
        return data

    def get_vars(self, obj):
        if not obj:
            return
        merged_vars = copy.deepcopy(obj.vars)
        for var_secret in obj.var_secrets:
            secret_name = var_secret.get('name', None)
            secret_namespace = var_secret.get('namespace', None)
            if secret_name:
                try:
                    secret_data = self.get_secret_data(secret_name, secret_namespace)
                    var_name = var_secret.get('var', None)
                    if var_name:
                        deep_update(merged_vars, {var_name: secret_data})
                    else:
                        deep_update(merged_vars, secret_data)
                except kubernetes.client.rest.ApiException as e:
                    if e.status != 404:
                        raise
                    operator_logger.warning('varSecrets references missing secret, %s', secret_name)
            else:
                operator_logger.warning('varSecrets has entry with no name')
        return merged_vars

    def register_runner(self, runner):
        self.anarchy_runners[runner] = time.time()

    def remove_runner(self, runner):
        try:
            del self.anarchy_runners[runner]
        except KeyError:
            pass

    def watch_peering(self):
        '''
        Wait for KopfPeering to indicate this pod should be active.
        '''
        for event in kubernetes.watch.Watch().stream(
            self.custom_objects_api.list_namespaced_custom_object,
            'zalando.org', 'v1', self.operator_namespace, 'kopfpeerings'
        ):
            obj = event.get('object')

            if event['type'] == 'ERROR' \
            and obj['kind'] == 'Status':
                if obj['status'] == 'Failure':
                    if obj['reason'] in ('Expired', 'Gone'):
                        operator_logger.info('KopfPeering watch restarting, reason %s', obj['reason'])
                        return
                    else:
                        raise Exception("KopfPeering watch failure: reason {}, message {}", obj['reason'], obj['message'])

            if obj \
            and obj.get('apiVersion') == 'zalando.org/v1' \
            and obj.get('kind') == 'KopfPeering' \
            and obj['metadata']['name'] == self.anarchy_service_name:
                with self.is_active_condition:
                    active_peer = None
                    priority = 0
                    for peerid, status in obj.get('status', {}).items():
                        if status['priority'] > priority:
                            active_peer = peerid
                            priority = status['priority']
                    if active_peer and '@{}/'.format(self.pod_name) in active_peer:
                        if not self.is_active:
                            operator_logger.info('Became active kopf peer: %s', active_peer)
                        self.is_active = True
                    else:
                        if self.is_active:
                            operator_logger.info('Active kopf peer is now: %s', active_peer)
                        self.is_active = False
                    self.is_active_condition.notify()