#!/usr/bin/env python3
"""
Proton Bridge → Stalwart IMAP migration
"""
import imaplib, ssl, email, time, sys, traceback
from datetime import datetime

SRC_HOST = '127.0.0.1'; SRC_PORT = 1143
SRC_USER = 'tj@jongsma.me'; SRC_PASS = 'BlcMCKtNDfqv0cq1LmGR9g'
DST_HOST = 'mail.jongsma.me'; DST_PORT = 993
DST_USER = 'tj'; DST_PASS = 'TJ-Migrate-2026!'

SKIP_FOLDERS = {'All Mail', 'Starred', 'Labels/Sent Messages',
                'Labels/Deleted Messages', 'Labels/Snoozed'}
SKIP_PREFIXES = ('Labels/',)

BATCH = 10  # smaller batch = more reliable with Proton Bridge

def src_ctx():
    c = ssl.create_default_context()
    c.check_hostname = False; c.verify_mode = ssl.CERT_NONE
    return c

def connect_src():
    s = imaplib.IMAP4(SRC_HOST, SRC_PORT)
    s.starttls(src_ctx()); s.login(SRC_USER, SRC_PASS)
    return s

def connect_dst():
    s = imaplib.IMAP4_SSL(DST_HOST, DST_PORT, ssl_context=ssl.create_default_context())
    s.login(DST_USER, DST_PASS)
    return s

def ensure_dst_folder(dst, folder):
    st, _ = dst.select(folder)
    if st == 'OK': return
    dst.create(folder)
    dst.select(folder)

def get_dst_message_ids(dst, folder):
    ids = set()
    st, _ = dst.select(folder)
    if st != 'OK': return ids
    st2, data = dst.search(None, 'ALL')
    if st2 != 'OK' or not data[0]: return ids
    msgs = data[0].split()
    for i in range(0, len(msgs), 200):
        chunk = b','.join(msgs[i:i+200])
        try:
            st3, hd = dst.fetch(chunk, '(BODY[HEADER.FIELDS (MESSAGE-ID)])')
            for item in hd:
                if isinstance(item, tuple):
                    raw = item[1].decode('utf-8', errors='replace')
                    for line in raw.splitlines():
                        if line.lower().startswith('message-id:'):
                            ids.add(line.split(':',1)[1].strip())
        except: pass
    return ids

def sync_folder(src, dst, folder):
    copied = skipped = errors = 0
    src_q = f'"{folder}"' if ' ' in folder or '/' in folder else folder

    st, _ = src.select(src_q, readonly=True)
    if st != 'OK':
        print(f'  ✗ Cannot select src: {folder}'); return 0, 0, 1

    st2, data = src.search(None, 'ALL')
    if st2 != 'OK' or not data[0]: return 0, 0, 0
    msg_ids = data[0].split()
    src_count = len(msg_ids)

    ensure_dst_folder(dst, folder)
    existing_ids = get_dst_message_ids(dst, folder)
    dst.select(folder)
    # Re-select source after dst operations (connection may have gone idle)
    src.select(src_q, readonly=True)

    for i in range(0, len(msg_ids), BATCH):
        chunk = msg_ids[i:i+BATCH]
        chunk_str = b','.join(chunk)
        try:
            st3, msg_data = src.fetch(chunk_str, '(INTERNALDATE FLAGS RFC822)')
        except Exception as e:
            errors += len(chunk)
            # Reconnect source
            try: src = connect_src(); src.select(src_q, readonly=True)
            except: pass
            continue

        for item in msg_data:
            if not isinstance(item, tuple): continue
            try:
                meta = item[0].decode('utf-8', errors='replace')
                raw_msg = item[1]

                # Stalwart v0.15.5 rejects \Flag syntax in APPEND — skip flags
                append_flags = None

                # Duplicate check by Message-ID
                parsed = email.message_from_bytes(raw_msg)
                mid = parsed.get('Message-ID', '').strip()
                if mid and mid in existing_ids:
                    skipped += 1; continue

                # Append
                dst.append(folder, append_flags, None, raw_msg)
                if mid: existing_ids.add(mid)
                copied += 1
            except Exception as e:
                errors += 1
                if errors <= 3:
                    sys.stdout.write(f'\n  ERR: {e}\n')
                    sys.stdout.flush()

        done = min(i+BATCH, src_count)
        sys.stdout.write(f'\r  {done}/{src_count} | ✓{copied} ⏭{skipped} ✗{errors}   ')
        sys.stdout.flush()

    print(f'\r  {src_count} src | ✓{copied} ⏭{skipped} ✗{errors}           ')
    return copied, skipped, errors

def main():
    print('=== Proton → Stalwart Migration ===')
    print(f'Started: {datetime.now():%Y-%m-%d %H:%M:%S}')
    src = connect_src(); dst = connect_dst()

    _, folders_raw = src.list()
    folders = []
    for f in folders_raw:
        d = f.decode()
        if 'Noselect' in d: continue
        parts = d.split('" "')
        if len(parts) < 2: continue
        folder = parts[-1].strip().strip('"')
        if folder in SKIP_FOLDERS: continue
        if any(folder.startswith(p) for p in SKIP_PREFIXES): continue
        folders.append(folder)

    print(f'Syncing {len(folders)} folders\n')
    results = []; t0 = time.time()
    total_c = total_s = total_e = 0

    for folder in folders:
        print(f'[{folder}]')
        try:
            c, s, e = sync_folder(src, dst, folder)
        except Exception as ex:
            print(f'  ✗ Fatal: {ex}')
            c, s, e = 0, 0, 1
            try: src = connect_src(); dst = connect_dst()
            except: pass
        results.append((folder, c, s, e))
        total_c += c; total_s += s; total_e += e

    elapsed = time.time() - t0
    print('\n' + '='*55)
    print('MIGRATION REPORT')
    print('='*55)
    print(f'Completed: {datetime.now():%Y-%m-%d %H:%M:%S}')
    print(f'Duration:  {int(elapsed//60)}m {int(elapsed%60)}s')
    print(f'Folders:   {len(folders)}')
    print(f'Copied:    {total_c}')
    print(f'Skipped:   {total_s} (duplicates)')
    print(f'Errors:    {total_e}')
    print()
    print(f'  {"Folder":<45} {"Copied":>7} {"Dup":>5} {"Err":>5}')
    print(f'  {"-"*45} {"-"*7} {"-"*5} {"-"*5}')
    for folder, c, s, e in results:
        mark = ' ⚠' if e > 0 else ''
        print(f'  {folder:<45} {c:>7} {s:>5} {e:>5}{mark}')
    src.logout(); dst.logout()

if __name__ == '__main__':
    main()
