# -*- coding: utf-8 -*-
import copy
import json
import os
import time
from collections import OrderedDict
from datetime import datetime

import requests
from artifactory import ArtifactoryPath
from requests.auth import HTTPBasicAuth

from crosspm.adapters.common import BaseAdapter
from crosspm.helpers.exceptions import *
from crosspm.helpers.package import Package

CHUNK_SIZE = 1024

setup = {
    "name": [
        "artifactory-aql",
        "jfrog-artifactory-aql",
    ],
}

session = requests.Session()


class Adapter(BaseAdapter):
    def get_packages(self, source, parser, downloader, list_or_file_path):
        _auth_type = source.args['auth_type'].lower() if 'auth_type' in source.args else 'simple'
        _art_auth_etc = {}
        if 'auth' in source.args:
            if _auth_type == 'simple':
                _art_auth_etc['auth'] = HTTPBasicAuth(*tuple(source.args['auth']))
                session.auth = _art_auth_etc['auth']
                # elif _auth_type == 'cert':
                #     _art_auth_etc['cert'] = os.path.realpath(os.path.expanduser(source.args['auth']))
        if 'auth' not in _art_auth_etc:
            msg = 'You have to set auth parameter for sources with artifactory-aql adapter'
            # self._log.error(msg)
            raise CrosspmException(
                CROSSPM_ERRORCODE_ADAPTER_ERROR,
                msg
            )

        if 'verify' in source.args:
            _art_auth_etc['verify'] = source.args['verify'].lower in ['true', 'yes', '1']
        else:
            _art_auth_etc['verify'] = False

        _pkg_name_col = self._config.name_column
        _packages_found = OrderedDict()
        _pkg_name_old = ""
        _packed_exist = False
        _packed_cache_params = None

        for _paths in parser.get_paths(list_or_file_path, source):

            _packages = []
            _params_found = {}
            _params_found_raw = {}
            last_error = ''
            _pkg_name = _paths['params'][_pkg_name_col]
            if _pkg_name != _pkg_name_old:
                _pkg_name_old = _pkg_name
                self._log.info(
                    '{}: {}'.format(_pkg_name,
                                    {k: v for k, v in _paths['params'].items() if
                                     k not in (_pkg_name_col, 'repo')}))
            for _sub_paths in _paths['paths']:
                _tmp_params = dict(_paths['params'])
                self._log.info('repo: {}'.format(_sub_paths['repo']))
                for _path in _sub_paths['paths']:
                    _tmp_params['repo'] = _sub_paths['repo']
                    # ------ START ----
                    # HACK for prefer-local
                    if self._config.prefer_local:
                        params = parser.get_params_with_extra('path', _paths['params'])
                        for param in params:
                            param['repo'] = _tmp_params['repo']
                            _path_packed = downloader.cache.path_packed(None, param)
                            _packed_exist = os.path.isfile(_path_packed) and self._config.prefer_local
                            if _packed_exist:
                                self._log.info("Skip searching, use package cache in path {}".format(_path_packed))
                                _packed_cache_params = param
                                break  # break check local cache
                    if _packed_exist:
                        break  # break connect to artifactory
                        # ------ END  ----
                    _path_fixed, _path_pattern, _file_name_pattern = parser.split_fixed_pattern_with_file_name(_path)
                    try:
                        _artifactory_server = _tmp_params['server']
                        _search_repo = _tmp_params['repo']

                        # Get AQL path pattern, with fixed part path, without artifactory url and repository name
                        _aql_path_pattern = _path_fixed[len(_artifactory_server) + 1 + len(_search_repo) + 1:]
                        if _path_pattern:
                            _aql_path_pattern = _aql_path_pattern + "/" + _path_pattern

                        _aql_query_url = '{}/api/search/aql'.format(_artifactory_server)
                        _aql_query_dict = {
                            "repo": {
                                "$eq": _search_repo,
                            },
                            "path": {
                                "$match": _aql_path_pattern,
                            },
                            "name": {
                                "$match": _file_name_pattern,
                            },
                        }
                        query = 'items.find({query_dict}).include("*", "property")'.format(
                            query_dict=json.dumps(_aql_query_dict))
                        session.auth = _art_auth_etc['auth']
                        r = session.post(_aql_query_url, data=query, verify=_art_auth_etc['verify'])
                        r.raise_for_status()

                        _found_paths = r.json()
                        for _found in _found_paths['results']:
                            _repo_path = "{artifactory}/{repo}/{path}/{file_name}".format(
                                artifactory=_artifactory_server,
                                repo=_found['repo'],
                                path=_found['path'],
                                file_name=_found['name'])
                            _repo_path = ArtifactoryPath(_repo_path, **_art_auth_etc)

                            _mark = 'found'
                            _matched, _params, _params_raw = parser.validate_path(str(_repo_path), _tmp_params)
                            if _matched:
                                _params_found[_repo_path] = {k: v for k, v in _params.items()}
                                _params_found_raw[_repo_path] = {k: v for k, v in _params_raw.items()}
                                _mark = 'match'

                                # TODO: _params_raw
                                if parser.has_rule('properties'):
                                    _found_properties = {x['key']: x.get('value', '') for x in _found['properties']}
                                    _valid, _params = parser.validate(_found_properties, 'properties', _tmp_params,
                                                                      return_params=True)
                                else:
                                    _valid, _params = True, {}
                                if _valid:
                                    _mark = 'valid'
                                    _packages += [_repo_path]
                                    _params_found[_repo_path].update({k: v for k, v in _params.items()})
                                    _params_found[_repo_path]['filename'] = str(_repo_path.name)
                            self._log.debug('  {}: {}'.format(_mark, str(_repo_path)))
                    except RuntimeError as e:
                        try:
                            err = json.loads(e.args[0])
                        except:
                            err = {}
                        if isinstance(err, dict):
                            # Check errors
                            # :e.args[0]: {
                            #                 "errors" : [ {
                            #                     "status" : 404,
                            #                     "message" : "Not Found"
                            #                  } ]
                            #             }
                            for error in err.get('errors', []):
                                err_status = error.get('status', -1)
                                err_msg = error.get('message', '')
                                if err_status == 401:
                                    msg = 'Authentication error[{}]{}'.format(err_status,
                                                                              (': {}'.format(
                                                                                  err_msg)) if err_msg else '')
                                elif err_status == 404:
                                    msg = last_error
                                else:
                                    msg = 'Error[{}]{}'.format(err_status,
                                                               (': {}'.format(err_msg)) if err_msg else '')
                                if last_error != msg:
                                    self._log.error(msg)
                                last_error = msg

            _package = None

            # HACK for prefer-local
            if _packed_exist:
                # HACK - Normalize params for cached archive
                for key, value in _packed_cache_params.items():
                    if isinstance(value, list):
                        value = ['' if x is None else x for x in value]
                        _packed_cache_params[key] = value
                _package = Package(_pkg_name, None, _paths['params'], downloader, self, parser,
                                   _packed_cache_params, list_or_file_path['raw'], {}, in_cache=True)
            # END HACK
            if _packages:
                _tmp = copy.deepcopy(_params_found)
                _packages = parser.filter_one(_packages, _paths['params'], _tmp)
                if isinstance(_packages, dict):
                    _packages = [_packages]

                if len(_packages) == 1:
                    _stat_pkg = self.pkg_stat(_packages[0]['path'])

                    _params_raw = _params_found_raw.get(_packages[0]['path'], {})
                    _params_tmp = _params_found.get(_packages[0]['path'], {})
                    _params_tmp.update({k: v for k, v in _packages[0]['params'].items() if k not in _params_tmp})
                    _package = Package(_pkg_name, _packages[0]['path'], _paths['params'], downloader, self, parser,
                                       _params_tmp, _params_raw, _stat_pkg)
                    _mark = 'chosen'
                    self._log.info('  {}: {}'.format(_mark, str(_packages[0]['path'])))

                elif len(_packages) > 1:
                    raise CrosspmException(
                        CROSSPM_ERRORCODE_MULTIPLE_DEPS,
                        'Multiple instances found for package [{}] not found.'.format(_pkg_name)
                    )
                else:
                    # Package not found: may be error, but it could be in other source.
                    pass
            else:
                # Package not found: may be error, but it could be in other source.
                pass

            if (_package is not None) or (not self._config.no_fails):
                _added, _package = downloader.add_package(_pkg_name, _package)
            else:
                _added = False
            if _package is not None:
                _pkg_name = _package.get_name_and_path(True)
            if _added or (_package is not None):
                if (_package is not None) or (not self._config.no_fails):
                    if (_package is not None) or (_packages_found.get(_pkg_name, None) is None):
                        _packages_found[_pkg_name] = _package

            if _added and (_package is not None):
                if downloader.do_load:
                    _package.download()

                    _deps_file = _package.get_file(self._config.deps_lock_file_name)
                    if _deps_file:
                        _package.find_dependencies(_deps_file)
                    elif self._config.deps_file_name:
                        _deps_file = _package.get_file(self._config.deps_file_name)
                        if _deps_file and os.path.isfile(_deps_file):
                            _package.find_dependencies(_deps_file)

        # HACK for not found packages
        _package_names = [x[self._config.name_column] for x in list_or_file_path['raw']]
        _packages_found_names = [x.package_name for x in _packages_found.values()]
        for package in _package_names:
            if package not in _packages_found_names:
                _packages_found[package] = None

        return _packages_found

    @staticmethod
    def pkg_stat(package):
        _stat_attr = {'ctime': 'st_atime',
                      'mtime': 'st_mtime',
                      'size': 'st_size'}
        _stat_pkg = package.stat()
        _stat_pkg = {k: getattr(_stat_pkg, k, None) for k in _stat_attr.keys()}
        _stat_pkg = {
            k: time.mktime(v.timetuple()) + float(v.microsecond) / 1000000.0 if isinstance(v, datetime) else v
            for k, v in _stat_pkg.items()
            }
        return _stat_pkg

    def download_package(self, package, dest_path):
        dest_dir = os.path.dirname(dest_path)

        if not os.path.exists(dest_dir):
            os.makedirs(dest_dir)
        elif os.path.exists(dest_path):
            os.remove(dest_path)

        try:
            _stat_pkg = self.pkg_stat(package)
            # with package.open() as _src:
            session.auth = package.auth
            _src = session.get(str(package), verify=package.verify, stream=True)
            _src.raise_for_status()

            with open(dest_path, 'wb+') as _dest:
                for chunk in _src.iter_content(CHUNK_SIZE):
                    if chunk:  # filter out keep-alive new chunks
                        _dest.write(chunk)
                        _dest.flush()

            _src.close()
            os.utime(dest_path, (_stat_pkg['ctime'], _stat_pkg['mtime']))

        except Exception as e:
            code = CROSSPM_ERRORCODE_SERVER_CONNECT_ERROR
            msg = 'FAILED to download package {} at url: [{}]'.format(
                package.name,
                str(package),
            )
            raise CrosspmException(code, msg) from e

        return dest_path

    @staticmethod
    def get_package_filename(package):
        if isinstance(package, ArtifactoryPath):
            return package.name
        return ''

    @staticmethod
    def get_package_path(package):
        if isinstance(package, ArtifactoryPath):
            return str(package)
        return ''
