import os
import logging
from pathlib import Path
from shutil import copytree, rmtree

from g import internal_tls_dir, DEFAULT_GID, DEFAULT_UID, PG_GID, PG_UID
from utils.misc import check_permission, owner_can_read, get_realpath, port_number_valid
from utils.cert import san_existed


class InternalTLS:

    harbor_certs_filename = {
        'harbor_internal_ca.crt',
        'proxy.crt', 'proxy.key',
        'core.crt', 'core.key',
        'job_service.crt', 'job_service.key',
        'registryctl.crt', 'registryctl.key',
        'registry.crt', 'registry.key',
        'portal.crt', 'portal.key'
    }

    trivy_certs_filename = {
        'trivy_adapter.crt', 'trivy_adapter.key',
    }

    db_certs_filename = {
        'harbor_db.crt', 'harbor_db.key'
    }

    def __init__(self, tls_enabled=False, verify_client_cert=False, tls_dir='', data_volume='', **kwargs):
        self.data_volume = data_volume
        self.verify_client_cert = verify_client_cert
        self.enabled = tls_enabled
        self.tls_dir = tls_dir
        if self.enabled:
            self.required_filenames = self.harbor_certs_filename
            if kwargs.get('with_trivy'):
                self.required_filenames.update(self.trivy_certs_filename)
            if not kwargs.get('external_database'):
                self.required_filenames.update(self.db_certs_filename)

    def __getattribute__(self, name: str):
        """
        Make the call like 'internal_tls.core_crt_path' possible
        """
        # only handle when enabled tls and name ends with 'path'
        if name.endswith('_path'):
            if not (self.enabled):
                return object.__getattribute__(self, name)

            name_parts = name.split('_')
            if len(name_parts) < 3:
                return object.__getattribute__(self, name)

            filename = '{}.{}'.format('_'.join(name_parts[:-2]), name_parts[-2])

            if filename in self.required_filenames:
                return os.path.join(self.data_volume, 'secret', 'tls', filename)

        return object.__getattribute__(self, name)

    def _check(self, filename: str):
        """
        Check cert and key files are correct
        """

        path = Path(os.path.join(internal_tls_dir, filename))

        if not path.exists:
            if filename == 'harbor_internal_ca.crt':
                return
            raise Exception('File {} not exist'.format(filename))

        if not path.is_file:
            raise Exception('invalid {}'.format(filename))

        # check key file permission
        if filename.endswith('.key') and not check_permission(path, mode=0o600):
            raise Exception('key file {} permission is not 600'.format(filename))

        # check certificate file
        if filename.endswith('.crt'):
            if not owner_can_read(path.stat().st_mode):
                # check owner can read cert file
                raise Exception('File {} should readable by owner'.format(filename))
            if not san_existed(path):
                # check SAN included
                if filename == 'harbor_internal_ca.crt':
                    return
                raise Exception('cert file {} should include SAN'.format(filename))

    def validate(self):
        if not self.enabled:
            # pass the validation if not enabled
            return

        if not internal_tls_dir.exists():
            raise Exception('Internal dir for tls {} not exist'.format(internal_tls_dir))

        for filename in self.required_filenames:
            self._check(filename)

    def prepare(self):
        """
        Prepare moves certs in tls file to data volume with correct permission.
        """
        if not self.enabled:
            logging.info('internal tls NOT enabled...')
            return
        original_tls_dir = get_realpath(self.tls_dir)
        if internal_tls_dir.exists():
            rmtree(internal_tls_dir)
        copytree(original_tls_dir, internal_tls_dir, symlinks=True)

        for file in internal_tls_dir.iterdir():
            if file.name.endswith('.key'):
                file.chmod(0o600)
            elif file.name.endswith('.crt'):
                file.chmod(0o644)

            if file.name in self.db_certs_filename:
                os.chown(file, PG_UID, PG_GID)
            else:
                os.chown(file, DEFAULT_UID, DEFAULT_GID)


class Metric:
    def __init__(self, enabled: bool = False, port: int = 8080, path: str = "metrics"):
        self.enabled = enabled
        self.port = port
        self.path = path

    def validate(self):
        if not port_number_valid(self.port):
            raise Exception('Port number in metrics is not valid')


class JaegerExporter:
    def __init__(self, config: dict):
        if not config:
            self.enabled = False
            return
        self.enabled = True
        self.endpoint = config.get('endpoint')
        self.username = config.get('username')
        self.password = config.get('password')
        self.agent_host = config.get('agent_host')
        self.agent_port = config.get('agent_port')

    def validate(self):
        if not self.endpoint and not self.agent_host:
            raise Exception('Jaeger Colector Endpoint or Agent host not set, must set one')
        if self.endpoint and self.agent_host:
            raise Exception('Jaeger Colector Endpoint and Agent host both set, only can set one')


class OtelExporter:
    def __init__(self, config: dict):
        if not config:
            self.enabled = False
            return
        self.enabled = True
        self.endpoint = config.get('endpoint')
        self.url_path = config.get('url_path')
        self.compression = config.get('compression') or False
        self.insecure = config.get('insecure') or False
        self.timeout = config.get('timeout') or '10'

    def validate(self):
        if not self.endpoint:
            raise Exception('Trace endpoint not set')
        if not self.url_path:
            raise Exception('Trace url path not set')


class Trace:
    def __init__(self, config: dict):
        self.enabled = config.get('enabled') or False
        self.sample_rate = config.get('sample_rate', 1)
        self.namespace = config.get('namespace') or ''
        self.jaeger = JaegerExporter(config.get('jaeger'))
        self.otel = OtelExporter(config.get('otel'))
        self.attributes = config.get('attributes') or {}

    def validate(self):
        if not self.enabled:
            return
        if not self.jaeger.enabled and not self.otel.enabled:
            raise Exception('Trace enabled but no trace exporter set')
        elif self.jaeger.enabled and self.otel.enabled:
            raise Exception('Only can have one trace exporter at a time')
        elif self.jaeger.enabled:
            self.jaeger.validate()
        elif self.otel.enabled:
            self.otel.validate()


class PurgeUpload:
    def __init__(self, config: dict):
        if not config:
            self.enabled = False
        self.enabled = config.get('enabled')
        self.age = config.get('age') or '168h'
        self.interval = config.get('interval') or '24h'
        self.dryrun = config.get('dryrun') or False
        return

    def validate(self):
        if not self.enabled:
            return
        # age should end with h
        if not isinstance(self.age, str) or not self.age.endswith('h'):
            raise Exception('purge upload age should set with with nh, n is the number of hour')
        # interval should larger than 2h
        age = self.age[:-1]
        if not age.isnumeric() or int(age) < 2:
            raise Exception('purge upload age should set with with nh, n is the number of hour and n should not be less than 2')

        # interval should end with h
        if not isinstance(self.interval, str) or not self.interval.endswith('h'):
            raise Exception('purge upload interval should set with with nh, n is the number of hour')
        # interval should larger than 2h
        interval = self.interval[:-1]
        if not interval.isnumeric() or int(interval) < 2:
            raise Exception('purge upload interval should set with with nh, n is the number of hour and n should not beless than 2')
        return


class Cache:
    def __init__(self, config: dict):
        if not config:
            self.enabled = False
        self.enabled = config.get('enabled')
        self.expire_hours = config.get('expire_hours')

    def validate(self):
        if not self.enabled:
            return

        if not self.expire_hours or self.expire_hours <= 0:
            raise Exception('cache expire hours should be positive number')
        return

class Core:
    def __init__(self, config: dict):
        self.quota_update_provider = config.get('quota_update_provider') or 'db'

    def validate(self):
        if not self.quota_update_provider:
            return

        if self.quota_update_provider not in ['db', 'redis']:
            raise Exception('invalid quota update provider: {}'.format(self.quota_update_provider))