# -*- coding: utf-8 -*-
"""
domain_service.py
"""
from __future__ import print_function, unicode_literals, absolute_import, division

import io
import time
import traceback
from datetime import datetime, timedelta

from peewee import chunked, fn
from playhouse.shortcuts import model_to_dict

from domain_admin.enums.role_enum import RoleEnum
from domain_admin.enums.source_enum import SourceEnum
from domain_admin.log import logger
from domain_admin.model import domain_model
from domain_admin.model.address_model import AddressModel
from domain_admin.model.domain_info_model import DomainInfoModel
from domain_admin.model.domain_model import DomainModel
from domain_admin.model.group_model import GroupModel
from domain_admin.model.group_user_model import GroupUserModel
from domain_admin.model.user_model import UserModel
from domain_admin.service import file_service, async_task_service
from domain_admin.service import render_service, group_service
from domain_admin.utils import datetime_util, cert_util, file_util, json_util, time_util
from domain_admin.utils import domain_util
from domain_admin.config import SHODAN_API_KEY
from domain_admin.utils.cert_util import cert_socket_v2, cert_openssl_v2, cert_ftp
from domain_admin.utils.flask_ext.app_exception import ForbiddenAppException
from domain_admin.utils.open_api import certspotter_api, shodan_api, crtsh_api


def update_domain_host_list(domain_row):
    """
    更新ip信息
    :param domain_row: DomainModel
    :return:
    @since v1.2.24
    """
    domain_host_list = []

    try:
        domain_host_list = cert_socket_v2.get_domain_host_list(
            domain=domain_row.domain,
            port=domain_row.port
        )
    except Exception as e:
        logger.error(traceback.format_exc())

    lst = [
        {
            'domain_id': domain_row.id,
            'host': domain_host
        } for domain_host in domain_host_list]

    logger.info(lst)

    AddressModel.insert_many(lst).on_conflict_ignore().execute()


def update_domain_address_list_cert(domain_row):
    """
    更新证书信息
    :param domain_row: DomainModel
    :return:
    """
    # logger.info("%s", model_to_dict(domain_row))

    lst = AddressModel.select().where(
        AddressModel.domain_id == domain_row.id
    )

    err = ''
    for address_row in lst:
        err = update_address_row_info_wrap(address_row, domain_row)

    sync_address_info_to_domain_info(domain_row)
    return err


def update_address_row_info_wrap(address_row, domain_row):
    """
    更新单个地址信息 的代理方法 增加重试次数
    :param address_row:
    :param domain_row:
    :return: error
    """
    # 最大重试次数
    MAX_RETRY_COUNT = 3
    retry_count = 0
    err = ''

    while True:
        retry_count += 1
        logger.info("retry_count: %s", retry_count)

        err = update_address_row_info(address_row, domain_row)

        if not err or retry_count >= MAX_RETRY_COUNT:
            break

        if _is_unreachable_handshake_error(err):
            break

        # sleep
        time.sleep(0.5)

    return err


def _is_unreachable_handshake_error(err_msg):
    if not err_msg:
        return False

    err_msg = err_msg.lower()

    if 'domain not verified' in err_msg:
        return False

    unreachable_keywords = [
        'timed out',
        'timeout',
        'name or service not known',
        'temporary failure in name resolution',
        'nodename nor servname provided',
        'connection refused',
        'connection reset',
        'network is unreachable',
        'no route to host',
        'host unreachable',
        'errno 111',
        'errno 113',
        'errno -2',
    ]

    return any(keyword in err_msg for keyword in unreachable_keywords)



def _get_latest_certspotter_cert_info(domain):
    if domain_util.is_ipv4(domain):
        return {}

    try:
        rows = certspotter_api.search(domain)
    except Exception:
        logger.error(traceback.format_exc())
        return {}

    if not rows:
        return {}

    candidates = []

    for row in rows:
        names = [item.strip() for item in row.get('dns_names', []) if item and item.strip()]

        matched = False
        for name in names:
            if domain_util.verify_cert_common_name(name, domain):
                matched = True
                break

        if not matched:
            continue

        not_before = row.get('not_before')
        not_after = row.get('not_after')
        if not not_before or not not_after:
            continue

        try:
            start_date = time_util.parse_time(not_before)
            expire_date = time_util.parse_time(not_after)

            entry_timestamp = row.get('entry_timestamp')
            if entry_timestamp:
                entry_time = time_util.parse_time(entry_timestamp)
            else:
                entry_time = expire_date

            row_id = row.get('id') or 0
            candidates.append({
                'start_date': start_date,
                'expire_date': expire_date,
                'entry_time': entry_time,
                'row_id': int(row_id),
            })

        except Exception:
            logger.error(traceback.format_exc())

    if not candidates:
        return {}

    latest = max(
        candidates,
        key=lambda item: (
            item['entry_time'],
            item['start_date'],
            item['expire_date'],
            item['row_id'],
        )
    )

    return {
        'start_date': latest['start_date'],
        'expire_date': latest['expire_date'],
    }



def _get_latest_shodan_cert_info(domain):
    if domain_util.is_ipv4(domain):
        return {}

    query_domains = [domain]
    root_domain = domain_util.get_root_domain(domain)
    if root_domain and root_domain not in query_domains:
        query_domains.append(root_domain)

    candidates = []

    for query_domain in query_domains:
        rows = shodan_api.search_domain_certificates(domain=query_domain, api_key=SHODAN_API_KEY)
        if not rows:
            continue

        for row in rows:
            subdomain = row.get('subdomain')
            if subdomain and subdomain != '@':
                record_domain = '{subdomain}.{domain}'.format(subdomain=subdomain, domain=query_domain)
            else:
                record_domain = query_domain

            if not domain_util.verify_cert_common_name(record_domain, domain):
                continue

            value = row.get('value') or ''
            value = value.strip().lower()
            if len(value) != 40:
                continue

            cert_detail = shodan_api.get_certificate_by_sha1(value, SHODAN_API_KEY)
            if not cert_detail:
                continue

            cert_name = cert_detail.get('subject', {}).get('CN')
            if cert_name and not domain_util.verify_cert_common_name(cert_name, domain):
                continue

            not_before = cert_detail.get('issued') or cert_detail.get('validity', {}).get('start')
            not_after = cert_detail.get('expires') or cert_detail.get('validity', {}).get('end')
            if not not_before or not not_after:
                continue

            try:
                start_date = time_util.parse_time(not_before)
                expire_date = time_util.parse_time(not_after)
                candidates.append({
                    'start_date': start_date,
                    'expire_date': expire_date,
                    'fingerprint': value,
                })
            except Exception:
                logger.error(traceback.format_exc())

    if not candidates:
        return {}

    latest = max(
        candidates,
        key=lambda item: (
            item['start_date'],
            item['expire_date'],
            item['fingerprint'],
        )
    )

    return {
        'start_date': latest['start_date'],
        'expire_date': latest['expire_date'],
    }



def _get_latest_fallback_cert_info(domain):
    cert_info = _get_latest_certspotter_cert_info(domain)
    if cert_info:
        return cert_info

    return _get_latest_shodan_cert_info(domain)



def update_address_row_info(address_row, domain_row):
    """
    更新单个地址信息
    :param domain_row:
    :param address_row:
    :return: error
    """

    # 获取证书信息
    cert_info = {}

    err = ''
    try:
        if domain_row.port == 21:
            cert_info = cert_ftp.get_ftp_cert_by_ftplib(
                    domain=domain_row.domain,
                    host=address_row.host,
                    port=domain_row.port,
            )

        else:
            cert_info = cert_openssl_v2.get_ssl_cert_by_openssl(
                domain=domain_row.domain,
                host=address_row.host,
                port=domain_row.port,
                ssl_type=domain_row.ssl_type
            )
    except Exception as e:
        err = e.__str__()
        logger.error(traceback.format_exc())

    fallback_applied = False

    if not cert_info and _is_unreachable_handshake_error(err):
        cert_info = _get_latest_fallback_cert_info(domain_row.domain)
        fallback_applied = True

    logger.info(cert_info)

    if fallback_applied and not cert_info:
        err = ''

    update_time = datetime_util.get_datetime()
    update_data = {
        'update_time': update_time,
    }

    if cert_info.get('expire_date'):
        address = AddressModel()
        address.ssl_start_time = cert_info.get('start_date')
        address.ssl_expire_time = cert_info.get('expire_date')

        update_data.update({
            'ssl_start_time': address.ssl_start_time,
            'ssl_expire_time': address.ssl_expire_time,
            'ssl_expire_days': address.real_time_ssl_expire_days,
        })

    AddressModel.update(
        **update_data
    ).where(
        AddressModel.id == address_row.id
    ).execute()

    if fallback_applied and cert_info.get('expire_date'):
        AddressModel.update(
            ssl_start_time=update_data.get('ssl_start_time'),
            ssl_expire_time=update_data.get('ssl_expire_time'),
            ssl_expire_days=update_data.get('ssl_expire_days'),
            update_time=update_time,
        ).where(
            AddressModel.domain_id == address_row.domain_id,
            AddressModel.id != address_row.id,
            AddressModel.ssl_expire_time.is_null(False),
            AddressModel.ssl_expire_time < update_data.get('ssl_expire_time')
        ).execute()

    return err


def update_address_row_info_with_sync_domain_row(address_id):
    """
    更新主机信息并同步到与名表
    :param address_id: int
    :return:
    """
    address_row = AddressModel.get_by_id(address_id)

    domain_row = DomainModel.get_by_id(address_row.domain_id)

    update_address_row_info_wrap(address_row, domain_row)

    sync_address_info_to_domain_info(domain_row)


def sync_address_info_to_domain_info(domain_row):
    """
    同步主机信息到域名信息表
    :param domain_row: DomainModel
    :return:
    """
    first_address_row = AddressModel.select().where(
        AddressModel.domain_id == domain_row.id,
        AddressModel.ssl_expire_time.is_null(False)
    ).order_by(
        AddressModel.ssl_expire_days.asc()
    ).first()

    update_data = {
        'connect_status': False,
        'update_time': datetime_util.get_datetime(),
        'version': DomainModel.version + 1
    }

    if first_address_row is not None:
        if first_address_row.real_time_ssl_expire_days > 0:
            update_data['connect_status'] = True

        update_data.update({
            'start_time': first_address_row.ssl_start_time,
            'expire_time': first_address_row.ssl_expire_time,
            'expire_days': first_address_row.real_time_ssl_expire_days,
        })

    DomainModel.update(
        **update_data
    ).where(
        DomainModel.id == domain_row.id
    ).execute()


def update_domain_row(domain_row):
    """
    更新域名相关数据
    :param domain_row: DomainModel
    :return:
    """
    # fix old data update root domain
    if not domain_row.root_domain:
        DomainModel.update(
            root_domain=domain_util.get_root_domain(domain_row.domain)
        ).where(
            DomainModel.id == domain_row.id
        ).execute()

    # 动态主机ip，需要先删除所有主机地址
    if domain_row.is_dynamic_host:
        pass

    # 移除动态主机行为，都清空自动添加的数据再获取
    AddressModel.delete().where(
        AddressModel.domain_id == domain_row.id,
        AddressModel.source == SourceEnum.AUTO
    ).execute()

    # 主机ip信息
    update_domain_host_list(domain_row)

    # 证书信息
    update_domain_address_list_cert(domain_row)


def get_cert_info(domain):
    """
    :param domain: str
    :return:
    """
    now = datetime.now()
    info = {}
    expire_days = 0
    total_days = 0
    connect_status = True

    try:
        info = cert_util.get_cert_info(domain)

    except Exception:
        logger.error(traceback.format_exc())
        connect_status = False

    start_date = info.get('start_date')
    expire_date = info.get('expire_date')

    if start_date and expire_date:
        start_time = datetime_util.parse_datetime(start_date)
        expire_time = datetime_util.parse_datetime(expire_date)

        expire_days = (expire_time - now).days
        total_days = (expire_time - start_time).days

    return {
        'start_date': start_date,
        'expire_date': expire_date,
        'expire_days': expire_days,
        'total_days': total_days,
        'connect_status': connect_status,
        # 'ip': info.get('ip', ''),
        'info': info,
    }


def update_all_domain_cert_info():
    """
    更新所有域名信息
    :return:
    """
    rows = DomainModel.select().where(
        DomainModel.auto_update == True
    ).order_by(DomainModel.expire_days.asc())

    for row in rows:
        try:
            update_domain_row(row)
        except Exception as e:
            logger.error(traceback.format_exc())


@async_task_service.async_task_decorator("更新证书信息")
def update_all_domain_cert_info_of_user(user_id):
    """
    更新用户的所有证书信息
    :return:
    """
    rows = DomainModel.select().where(
        DomainModel.user_id == user_id,
        DomainModel.auto_update == True
    )

    for row in rows:
        update_domain_row(row)

    # key = f'update_domain_status:{user_id}'
    # global_data_service.set_value(key, False)


def get_domain_info_list(user_id=None):
    query = DomainModel.select()

    user_row = UserModel.get_by_id(user_id)

    query = query.where(
        DomainModel.user_id == user_id,
        DomainModel.is_monitor == True,
        DomainModel.expire_days < user_row.before_expire_days
    )

    query = query.order_by(
        DomainModel.expire_days.asc(),
        DomainModel.id.desc()
    )

    lst = list(map(lambda m: model_to_dict(
        model=m,
        # exclude=[DomainModel.detail_raw],
        extra_attrs=[
            'start_date',
            'expire_date',
            # 'real_time_domain_expire_days',
            'real_time_expire_days',
            # 'expire_days',
        ]
    ), query))

    # def compare(a, b):
    #     if a['expire_days'] and b['expire_days']:
    #         return a['expire_days'] - b['expire_days']
    #     else:
    #         if a['expire_days']:
    #             return a['expire_days']
    #         else:
    #             return -b['expire_days']

    # lst = sorted(lst, key=cmp_to_key(compare))

    return lst


def check_permission_and_get_row(domain_id, user_id):
    """
    权限检查
    :param domain_id:
    :param user_id:
    :return:
    """
    row = DomainModel.get_by_id(domain_id)
    if row.user_id != user_id:
        raise ForbiddenAppException()

    return row


@async_task_service.async_task_decorator("自动批量导入子域名证书")
def auto_import_from_domain_batch_async(rows, user_id):
    # type: (list[DomainInfoModel], int)->None
    """
    自动批量导入子域名证书
    :param rows:
    :param user_id:
    :return:
    """
    for row in rows:
        auto_import_from_domain(root_domain=row.domain, group_id=row.group_id, user_id=user_id)

    init_domain_cert_info_of_user(user_id=user_id)


@async_task_service.async_task_decorator("自动导入子域名证书")
def auto_import_from_domain_async(root_domain, group_id=0, user_id=0):
    # type: (str, int, int)->None
    auto_import_from_domain(root_domain=root_domain, group_id=group_id, user_id=user_id)

    init_domain_cert_info_of_user(user_id=user_id)


def auto_import_from_domain(root_domain, group_id=0, user_id=0):
    """
    自动导入顶级域名下包含的子域名到证书列表
    :param root_domain: str
    :param group_id: int
    :param user_id: int
    :return:
    """
    logger.info("domain: %s", root_domain)

    lst = crtsh_api.search(root_domain)

    domain_set = set()

    for domain in lst:
        common_name = domain['common_name']

        # 过滤空域名
        if not common_name:
            continue

        # 过滤非主域名下子域
        if not common_name.endswith(root_domain):
            continue

        # 过滤邮箱数据
        if '@' in common_name:
            continue

        # 识别通配符 *.music.163.com -> music.163.com
        if common_name.startswith('*.'):
            common_name = common_name[2:]

        domain_set.add(common_name)

    data = [
        {
            'domain': domain,
            'root_domain': root_domain,
            'port': 443,
            'alias': '',
            'user_id': user_id,
            'group_id': group_id,
        } for domain in domain_set
    ]

    for batch in chunked(data, 500):
        DomainModel.insert_many(batch).on_conflict_ignore().execute()


def add_domain_from_file(filename, user_id):
    logger.info('user_id: %s, filename: %s', user_id, filename)

    lst = list(domain_util.parse_domain_from_file(filename, domain_model.FIELD_MAPPING))

    # 导入分组
    group_name_list = [item.get('group_name') for item in lst if item.get('group_name')]
    if group_name_list:
        group_map = group_service.get_or_create_group_map(group_name_list, user_id)
    else:
        group_map = {}

    items = [
        {
            'domain': item['domain'],
            'root_domain': domain_util.get_root_domain(item['domain']),
            'port': item.get('port'),
            'alias': item.get('alias') or '',
            'user_id': user_id,
            'group_id': group_map.get(item.get('group_name'), 0),
        } for item in lst
    ]

    # print(json_util.json_dump(items))

    # fix: peewee.OperationalError: too many SQL variables
    # https://github.com/mouday/domain-admin/issues/63
    for batch in chunked(items, 500):
        DomainModel.insert_many(batch).on_conflict_ignore().execute()


def export_domain_to_file(rows, ext):
    """
    导出域名到文件
    :param rows:
    :return:
    """

    # content = render_service.render_template('cert-export.csv', {'list': rows})

    filename = datetime.now().strftime("cert_%Y%m%d%H%M%S") + '.' + ext
    temp_filename = file_service.resolve_temp_file(filename)

    if ext == 'txt':
        lst = [row['domain'] for row in rows]
    else:
        lst = file_util.convert_to_export(rows, domain_model.FIELD_MAPPING)

    file_util.write_data_to_file(temp_filename, lst)

    # temp_filename = file_service.resolve_temp_file(filename)
    # # print(temp_filename)
    # with io.open(temp_filename, 'w', encoding='utf-8') as f:
    #     f.write(content)

    return filename


def load_domain_expire_days(lst):
    """
    加载域名过期时间字段 Number or None
    :param lst: List[DomainModel dict]
    :return:
    """

    root_domains = [row['root_domain'] for row in lst]

    domain_info_rows = DomainInfoModel.select().where(
        DomainInfoModel.domain.in_(root_domains)
    )

    domain_info_map = {
        row.domain: row.real_domain_expire_days
        for row in domain_info_rows
    }

    for row in lst:
        row['domain_expire_days'] = domain_info_map.get(row['root_domain'])

    return lst


def load_address_count(lst):
    """
    加载主机数量字段
    :param lst: List
    :return:
    """
    row_ids = [row['id'] for row in lst]

    # 主机数量
    address_groups = AddressModel.select(
        AddressModel.domain_id,
        fn.COUNT(AddressModel.id).alias('count')
    ).where(
        AddressModel.domain_id.in_(row_ids)
    ).group_by(AddressModel.domain_id)

    address_group_map = {
        str(row.domain_id): row.count
        for row in address_groups
    }

    for row in lst:
        row['address_count'] = address_group_map.get(str(row['id']), 0)

    return lst


def get_domain_list_query(keyword, group_id, group_ids, expire_days, user_id, role):
    user_group_ids = None

    if role == RoleEnum.ADMIN:
        pass

    else:
        # 所在分组
        group_user_rows = GroupUserModel.select().where(
            GroupUserModel.user_id == user_id
        )

        group_user_list = list(group_user_rows)
        user_group_ids = [row.group_id for row in group_user_list]

    query = DomainModel.select()

    if isinstance(group_id, int):
        query = query.where(DomainModel.group_id == group_id)

    if keyword:
        query = query.where(DomainModel.domain.contains(keyword))

    if group_ids:
        query = query.where(DomainModel.group_id.in_(group_ids))
    else:
        if role == RoleEnum.ADMIN:
            pass

        elif user_group_ids:
            query = query.where(
                (DomainModel.user_id == user_id)
                | (DomainModel.group_id.in_(user_group_ids))
            )
        else:
            query = query.where(DomainModel.user_id == user_id)

    if expire_days is not None:
        if expire_days[0] is None:
            max_expire_time = datetime.now() + timedelta(days=expire_days[1])
            query = query.where(
                DomainModel.expire_time.is_null(False),
                DomainModel.expire_time <= max_expire_time
            )
        elif expire_days[1] is None:
            min_expire_time = datetime.now() + timedelta(days=expire_days[0])
            query = query.where(DomainModel.expire_time >= min_expire_time)
        else:
            min_expire_time = datetime.now() + timedelta(days=expire_days[0])
            max_expire_time = datetime.now() + timedelta(days=expire_days[1])

            query = query.where(DomainModel.expire_time.between(min_expire_time, max_expire_time))

    return query


def get_domain_ordering(order_prop='expire_days', order_type='ascending'):
    ordering = []

    # order by expire_days
    if order_prop == 'expire_days':
        if order_type == 'descending':
            ordering.append(DomainModel.expire_time.desc())
        else:
            ordering.append(DomainModel.expire_time.asc())

    # order by connect_status
    elif order_prop == 'connect_status':
        if order_type == 'descending':
            ordering.append(DomainModel.connect_status.desc())
        else:
            ordering.append(DomainModel.connect_status.asc())

    # order by domain
    elif order_prop == 'domain':
        if order_type == 'descending':
            ordering.append(DomainModel.domain.desc())
        else:
            ordering.append(DomainModel.domain.asc())

    # order by group_id
    elif order_prop == 'group_name':
        if order_type == 'descending':
            ordering.append(DomainModel.group_id.desc())
        else:
            ordering.append(DomainModel.group_id.asc())

    # order by port
    elif order_prop == 'port':
        if order_type == 'descending':
            ordering.append(DomainModel.port.desc())
        else:
            ordering.append(DomainModel.port.asc())

    # order by update_time
    elif order_prop == 'update_time':
        if order_type == 'descending':
            ordering.append(DomainModel.update_time.desc())
        else:
            ordering.append(DomainModel.update_time.asc())

    # order by domain_expire_monitor
    elif order_prop == 'domain_expire_monitor':
        if order_type == 'descending':
            ordering.append(DomainModel.domain_expire_monitor.desc())
        else:
            ordering.append(DomainModel.domain_expire_monitor.asc())

    # order by auto_update
    elif order_prop == 'auto_update':
        if order_type == 'descending':
            ordering.append(DomainModel.auto_update.desc())
        else:
            ordering.append(DomainModel.auto_update.asc())
    # order by  is_monitor
    elif order_prop == 'is_monitor':
        if order_type == 'descending':
            ordering.append(DomainModel.is_monitor.desc())
        else:
            ordering.append(DomainModel.is_monitor.asc())

    ordering.append(DomainModel.id.desc())

    return ordering


def init_domain_cert_info_of_user(user_id):
    """
    初始化证书信息
    :param user_id:
    :return:
    """
    # 更新插入的证书
    rows = DomainModel.select().where(
        DomainModel.version == 0,
        DomainModel.user_id == user_id,
    )

    for row in rows:
        update_domain_row(row)


def has_read_permission(domain_row, user_id):
    """
    读取权限
    """
    # 1、域名所有人
    if domain_row.user_id == user_id:
        return True

    # 2、分组成员
    group_user_row = GroupUserModel.select().where(
        GroupUserModel.group_id == domain_row.group_id,
        GroupUserModel.user_id == user_id
    ).first()

    if group_user_row:
        return True

    return False


def has_edit_permission(domain_row, user_id):
    """
    编辑权限
    """
    # 1、域名所有人
    if domain_row.user_id == user_id:
        return True

    # 2、分组成员 并且拥有编辑权限
    group_user_row = GroupUserModel.select().where(
        GroupUserModel.group_id == domain_row.group_id,
        GroupUserModel.user_id == user_id,
        GroupUserModel.has_edit_permission == True
    ).first()

    if group_user_row:
        return True

    return False
