import email
import hashlib
import imaplib
import re
import smtplib

from gevent import socket, ssl

from . import conf, fn_time, imap, imap_utf7, local, lock, log, message, schema

SKIP_DRAFTS = True
thrid_re = re.compile(r'(^| )mlr/thrid/\d+')


@local.setting('remote/account')
def data_account(value):
    schema.validate(value, {
        'type': 'object',
        'properties': {
            'username': {'type': 'string'},
            'password': {'type': 'string'},
            'imap_host': {'type': 'string'},
            'imap_port': {'type': 'integer', 'default': 993},
            'smtp_host': {'type': 'string'},
            'smtp_port': {'type': 'integer', 'default': 587},
        },
        'required': ['username', 'password', 'imap_host', 'smtp_host']
    })
    value = value.copy()
    if value['imap_host'] == 'imap.gmail.com':
        value['gmail'] = True
    return value


@local.setting('remote/uidnext', lambda: {})
def data_uidnext(key, value):
    setting = data_uidnext.get()
    setting[key] = value
    return setting


@local.setting('remote/modseq', lambda: {})
def data_modseq(key, value):
    setting = data_modseq.get()
    setting[key] = value
    return setting


def box_key(box=None, tag=None):
    if not box and not tag:
        raise ValueError('"box" or "tag" should be specified')

    account = data_account.get()
    key = account['imap_host'], account['username'], tag or box
    return ':'.join(key)


class Remote(imaplib.IMAP4, imap.Conn):
    def __init__(self):
        account = data_account.get()
        self.username = account['username']
        self.password = account['password']
        self.defaults()
        super().__init__(account['imap_host'], account['imap_port'])

    def _create_socket(self):
        ssl_context = ssl.SSLContext()
        sock = socket.create_connection((self.host, self.port))
        return ssl_context.wrap_socket(sock, server_hostname=self.host)

    def open(self, host='', port=imaplib.IMAP4_SSL_PORT):
        super().open(host, port=imaplib.IMAP4_SSL_PORT)

    def login(self):
        return super().login(self.username, self.password)


def connect():
    con = Remote()
    imap.check(con.login())
    return con


def client(tag=None, box=None, writable=False, readonly=True):
    ctx = imap.client(connect, writable=writable)
    if box:
        ctx.select(box, readonly=readonly)
    elif tag:
        ctx.select_tag(tag, readonly=readonly)
    return ctx


@local.using(local.SRC)
def fetch_imap(uids, box, tag=None, con=None):
    map_tags = {
        '\\Inbox': '#inbox',
        '\\Junk': '#spam',
        '\\Trash': '#trash',
        '\\Sent': '#sent',
    }
    exists = {}
    res = con.fetch('1:*', 'BODY.PEEK[HEADER.FIELDS (X-SHA256)]')
    for i in range(0, len(res), 2):
        uid = res[i][0].decode().split()[2]
        line = res[i][1].strip()
        if not line:
            continue
        hash = email.message_from_bytes(line)['X-SHA256'].strip()
        exists[hash.strip('<>')] = uid

    def msgs(con):
        account = data_account.get()
        res = con.fetch(uids, '(UID INTERNALDATE FLAGS BODY.PEEK[])')
        for i in range(0, len(res), 2):
            line, raw = res[i]
            hash = hashlib.sha256(raw).hexdigest()
            if hash in exists:
                continue
            parts = re.search(
                r'('
                r'UID (?P<uid>\d+)'
                r' ?|'
                r'INTERNALDATE (?P<time>"[^"]+")'
                r' ?|'
                r'FLAGS \((?P<flags>[^)]*)\)'
                r' ?){3}',
                line.decode()
            ).groupdict()

            flags = parts['flags']
            if tag and tag in map_tags:
                flags = ' '.join([flags, map_tags[tag]])

            headers = [
                'X-SHA256: <%s>' % hash,
                'X-Remote-Host: <%s>' % account['imap_host'],
                'X-Remote-Login: <%s>' % account['username'],
            ]

            # line break should be in the end, so an empty string here
            headers.append('')
            headers = '\r\n'.join(headers)

            raw = headers.encode() + raw
            yield parts['time'], flags, raw

    with client(box=box, tag=tag) as c:
        msgs = list(msgs(c))
    if not msgs:
        return None

    return con.multiappend(local.SRC, msgs)


def uids_by_msgid_gmail(con):
    uids = {}
    res = con.fetch('1:*', 'BODY.PEEK[HEADER.FIELDS (X-GM-MSGID)]')
    for i in range(0, len(res), 2):
        uid = res[i][0].decode().split()[2]
        line = res[i][1].strip()
        if not line:
            continue
        gid = email.message_from_bytes(line)['X-GM-MSGID'].strip()
        uids[gid.strip('<>')] = uid
    return uids


def flags_by_gmail(tag, flags, labels):
    flags = flags or ''
    labels = labels or ''
    map_flags = {
        '\\Answered': '\\Answered',
        '\\Flagged': '\\Flagged',
        '\\Deleted': '\\Deleted',
        '\\Seen': '\\Seen',
        '\\Draft': '\\Draft',
    }
    map_labels = {
        '\\Drafts': '\\Draft',
        '\\Draft': '\\Draft',
        '\\Starred': '\\Flagged',
        '\\Inbox': '#inbox',
        '\\Junk': '#spam',
        '\\Trash': '#trash',
        '\\Sent': '#sent',
        '\\Chats': '#chats',
        '\\Important': '',
    }

    def flag(m):
        flag = m.group()
        if flag:
            return map_flags.get(flag, '')
        return ''

    def label(m):
        label = m.group()
        if label:
            label = label.strip('"').replace('\\\\', '\\')
            label = imap_utf7.decode(label)
            flag = map_labels.get(label, None)
            if flag is None:
                flag = local.get_tag(label)['id']
            return flag
        return ''

    flags = re.sub(r'([^ ])*', flag, flags)
    flags = ' '.join([
        flags,
        re.sub(r'("[^"]*"|[^" ]*)', label, labels),
        map_labels.get(tag, ''),
    ]).strip()
    return flags


@local.using(local.SRC)
def fetch_gmail(uids, box, tag, con=None):

    existing = uids_by_msgid_gmail(con)
    new_uids = []
    with client(tag, box=box) as gm:
        res = gm.fetch(uids.str, 'X-GM-MSGID')
        for line in res:
            parts = re.search(
                r'('
                r'UID (?P<uid>\d+)'
                r' ?|'
                r'X-GM-MSGID (?P<msgid>\d+)'
                r' ?){2}',
                line.decode()
            ).groupdict()
            if parts['msgid'] in existing:
                continue
            new_uids.append(parts['uid'])
        if not new_uids:
            log.debug('%s are alredy imported' % uids)
            return
        fields = (
            '('
            'INTERNALDATE FLAGS BODY.PEEK[] '
            'X-GM-LABELS X-GM-MSGID X-GM-THRID'
            ')'
        )
        res = gm.fetch(new_uids, fields)
        login = gm.username

    def msgs():
        for i in range(0, len(res), 2):
            line, raw = res[i]
            parts = re.search(
                r'('
                r'UID (?P<uid>\d+)'
                r' ?|'
                r'INTERNALDATE (?P<time>"[^"]+")'
                r' ?|'
                r'FLAGS \((?P<flags>[^)]*)\)'
                r' ?|'
                r'X-GM-LABELS \((?P<labels>.*)\)'
                r' ?|'
                r'X-GM-MSGID (?P<msgid>\d+)'
                r' ?|'
                r'X-GM-THRID (?P<thrid>\d+)'
                r' ?){6}',
                line.decode()
            ).groupdict()
            if not raw or parts['msgid'] in existing:
                # this happens in "[Gmail]/Chats" folder
                continue
            flags = flags_by_gmail(tag, parts['flags'], parts['labels'])
            if SKIP_DRAFTS and '\\Draft' in flags:
                # TODO: skip drafts for now
                continue

            headers = [
                'X-SHA256: <%s>' % hashlib.sha256(raw).hexdigest(),
                'X-GM-UID: <%s>' % parts['uid'],
                'X-GM-MSGID: <%s>' % parts['msgid'],
                'X-GM-THRID: <%s>' % parts['thrid'],
                'X-GM-Login: <%s>' % login,
            ]
            thrid = thrid_re.search(flags)
            if thrid:
                flags = thrid_re.sub('', flags)
                thrid = thrid.group().strip()
                headers.append('X-Thread-ID: <%s@mailur.link>' % thrid)

            # line break should be in the end, so an empty string here
            headers.append('')
            headers = '\r\n'.join(headers)

            raw = headers.encode() + raw
            yield parts['time'], flags, raw

    msgs = list(msgs())
    if not msgs:
        return None

    return con.multiappend(local.SRC, msgs)


@fn_time
@lock.user_scope('remote-fetch')
def fetch_folder(box=None, tag=None, **opts):
    account = data_account.get()
    uidnext_key = box_key(box, tag)
    uidvalidity, uidnext = data_uidnext.key(uidnext_key, (None, None))
    log.info('saved: uidvalidity=%s uidnext=%s', uidvalidity, uidnext)
    con = client(tag=tag, box=box)
    folder = {'uidnext': con.uidnext, 'uidval': con.uidvalidity}
    log.info('remote: uidvalidity=%(uidval)s uidnext=%(uidnext)s', folder)
    if folder['uidval'] != uidvalidity:
        uidvalidity = folder['uidval']
        uidnext = 1
    uids = con.search('UID %s:*' % uidnext)
    uids = [i for i in uids if int(i) >= uidnext]
    uidnext = folder['uidnext']
    log.info('box(%s): %s new uids', con.box, len(uids))
    con.logout()
    if len(uids):
        uids = imap.Uids(uids, **opts)
        fetch_uids = fetch_gmail if account.get('gmail') else fetch_imap
        uids.call_async(fetch_uids, uids, box, tag)

    data_uidnext(uidnext_key, (uidvalidity, uidnext))


def fetch(**kw):
    if kw.get('tag') or kw.get('box'):
        fetch_folder(**kw)
        return

    for params in get_folders():
        fetch_folder(**dict(kw, **params))


def get_folders():
    account = data_account.get()
    if not account:
        log.info('no remote account')
        return []

    if account.get('gmail'):
        return [{'tag': '\\All'}, {'tag': '\\Junk'}, {'tag': '\\Trash'}]
    else:
        with client(None) as c:
            if c.select_tag('\\All', exc=False):
                items = [{'tag': '\\All'}]
            else:
                items = [{'box': 'INBOX', 'tag': '\\Inbox'}]
                if c.select_tag('\\Sent', exc=False):
                    items.append({'tag': '\\Sent'})
        return items


@lock.user_scope('remote-sync')
@local.using(local.SRC, reuse=False)
def sync_gmail(con=None):
    uids_by_msgid = uids_by_msgid_gmail(con)
    flags_by_uid_remote = {}
    flags_by_uid_local = {}
    modseqs = {}

    label_by_flag = {
        '#trash': '\\Trash',
        '#spam': '\\Spam',
        '#inbox': '\\Inbox',
        '\\Flagged': '\\Starred',
    }
    folders = {'#trash', '#spam'}
    folder_gmail_tags = {'\\Trash', '\\Junk'}
    flags_in_sync = {'#trash', '#spam', '#inbox', '\\Flagged', '\\Seen'}

    def find_uid_remote(gm, msgid):
        uid = None
        for params in get_folders():
            tag = params['tag']
            gm.select_tag(tag=tag)
            res = gm.search('X-GM-MSGID %s' % msgid)
            if res:
                uid = res[0]
                break
        return uid, tag

    def gen_gmail_actions(actions, uid, flags, mark):
        if not flags:
            return
        flags = sorted(flags)
        labels = {label_by_flag[f] for f in flags if f in label_by_flag}
        key = ('%sX-GM-LABELS' % mark, ' '.join(labels))
        actions.setdefault(key, [])
        actions[key].append(uid)
        if '\\Seen' in flags:
            key = ('%sFLAGS.SILENT' % mark, '\\Seen')
            actions.setdefault(key, [])
            actions[key].append(uid)

    def sync_gmail_folder(gm, tag, flags_by_uid_local):
        actions = {}
        res = gm.fetch('1:*', '(UID X-GM-MSGID X-GM-LABELS FLAGS)')
        for line in res:
            parts = re.search(
                r'('
                r'UID (?P<uid>\d+)'
                r' ?|'
                r'FLAGS \((?P<flags>[^)]*)\)'
                r' ?|'
                r'X-GM-LABELS \((?P<labels>.*)\)'
                r' ?|'
                r'X-GM-MSGID (?P<msgid>\d+)'
                r' ?){4}',
                line.decode()
            ).groupdict()
            uid = parts['uid']
            local_uid = uids_by_msgid.get(parts['msgid'])
            if not local_uid:
                # skip, probably draft
                continue
            if local_uid not in flags_by_uid_local:
                continue
            flags_remote = flags_by_gmail(tag, parts['flags'], parts['labels'])
            flags_remote = set(flags_remote.split()) & flags_in_sync
            flags_local = flags_by_uid_local[local_uid]
            flags_to_add = flags_local - flags_remote
            if flags_to_add:
                inbox = {'#inbox'}
                if flags_local & folders and flags_to_add & inbox:
                    # remove \\Inbox first
                    gen_gmail_actions(actions, uid, flags_local & inbox, '-')
                    flags_to_add = flags_to_add - inbox
                gen_gmail_actions(actions, uid, flags_to_add, '+')
            flags_to_del = flags_remote - flags_local
            if flags_to_del:
                gen_gmail_actions(actions, uid, flags_to_del, '-')
                if flags_to_del & folders and tag in folder_gmail_tags:
                    # move to \\All first, by adding \\Inbox
                    gen_gmail_actions(actions, uid, {'#inbox'}, '+')

        gm.select(gm.box, readonly=False)
        for action, uids, in actions.items():
            gm.store(uids, *action)

    def gen_local_actions(actions, uid, flags, mark):
        if not flags:
            return
        flags = sorted(flags)
        key = ('%sFLAGS.SILENT' % mark, ' '.join(flags))
        actions.setdefault(key, [])
        actions[key].append(uid)

    @local.using(local.SRC, name='con_src', readonly=False, reuse=False)
    @local.using(local.ALL, name='con_all', readonly=False, reuse=False)
    def sync_local(flags_by_uid_remote, con_src=None, con_all=None):
        actions = {}
        res = con_src.fetch(flags_by_uid_remote.keys(), '(UID FLAGS)')
        for line in res:
            pattern = r'UID (\d+) FLAGS \(([^)]*)\)'
            uid, flags_local = re.search(pattern, line.decode()).groups()
            flags_local = set(flags_local.split()) & flags_in_sync
            flags_remote = flags_by_uid_remote[uid]
            flags_to_add = flags_remote - flags_local
            if flags_to_add:
                gen_local_actions(actions, uid, flags_to_add, '+')
            flags_to_del = flags_local - flags_remote
            if flags_to_del:
                gen_local_actions(actions, uid, flags_to_del, '-')
        for action, uids in actions.items():
            con_src.store(uids, *action)

            parsed_uids = local.pair_origin_uids(uids)
            con_all.store(parsed_uids, *action)

    def get_remote_flags_for_sync(box=None, tag=None):
        modseq_key = box_key(box, tag)
        modseq_gmail = data_modseq.key(modseq_key, 1)
        with client(tag, box=box) as gm:
            if modseq_gmail >= gm.highestmodseq:
                log.info('Nothing to sync on gmail: modseq=%s' % modseq_gmail)
                return

            modseqs[modseq_key] = gm.highestmodseq
            fields = (
                '(UID X-GM-MSGID X-GM-LABELS FLAGS) (CHANGEDSINCE %s)'
                % modseq_gmail
            )
            res = gm.fetch('1:*', fields)
            for line in res:
                parts = re.search(
                    r'('
                    r'UID (?P<uid>\d+)'
                    r' ?|'
                    r'FLAGS \((?P<flags>[^)]*)\)'
                    r' ?|'
                    r'X-GM-LABELS \((?P<labels>.*)\)'
                    r' ?|'
                    r'X-GM-MSGID (?P<msgid>\d+)'
                    r' ?|'
                    r'MODSEQ \(\d+\)'
                    r' ?){5}',
                    line.decode()
                ).groupdict()
                flags = flags_by_gmail(tag, parts['flags'], parts['labels'])
                uid = uids_by_msgid.get(parts['msgid'])
                if not uid:
                    # probably draft
                    continue
                flags_by_uid_remote[uid] = set(flags.split()) & flags_in_sync

    def get_local_flags_for_sync():
        modseq_key = box_key(tag='\\Local')
        modseq_local = data_modseq.key(modseq_key, 1)
        if modseq_local >= con.highestmodseq:
            log.info('Nothing to sync on local: modseq=%s' % modseq_local)
            return

        modseqs[modseq_key] = con.highestmodseq
        res = con.fetch('1:*', '(UID FLAGS) (CHANGEDSINCE %s)' % modseq_local)
        for line in res:
            val = re.search(
                r'UID (\d+) FLAGS \(([^)]*)\) MODSEQ \(\d+\)',
                line.decode()
            )
            if not val:
                continue
            uid, flags = val.groups()
            flags_by_uid_local[uid] = set(flags.split()) & flags_in_sync

    def sync_flags(flags_by_uid_local, flags_by_uid_remote):
        if not flags_by_uid_local and not flags_by_uid_remote:
            return

        local_changes = set(flags_by_uid_local)
        if local_changes:
            log.info('Sync flags to gmail: %s', local_changes)
            for params in get_folders():
                tag = params['tag']
                with client(**params, writable=True) as gm:
                    sync_gmail_folder(gm, tag, flags_by_uid_local)

        remote_changes = set(flags_by_uid_remote) - local_changes
        if remote_changes:
            log.info('Sync flags from gmail: %s', remote_changes)
            sync_local({k: flags_by_uid_remote[k] for k in remote_changes})

    for params in get_folders():
        get_remote_flags_for_sync(**params)

    get_local_flags_for_sync()

    sync_flags(flags_by_uid_local, flags_by_uid_remote)

    if modseqs:
        log.info('modseqs: %s', modseqs)
        for key, value in modseqs.items():
            data_modseq(key, value)


def sync(only_flags=False):
    if not only_flags:
        try:
            fetch()
            local.parse()
        except lock.Error as e:
            log.warn(e)

    account = data_account.get()
    if account.get('gmail') and conf['GMAIL_TWO_WAY_SYNC']:
        try:
            return sync_gmail()
        except lock.Error as e:
            log.warn(e)


def send(msg):
    params = message.sending(msg)

    account = data_account.get()
    con = smtplib.SMTP(account['smtp_host'], account['smtp_port'])
    con.ehlo()
    con.starttls()
    con.login(account['username'], account['password'])
    con.sendmail(*params)

    fetch()
    local.parse()