#!/usr/bin/env python3
"""
IMAP email migration tool - migrates emails with full folder structure
between IMAP servers while preserving flags, folder hierarchy, and dates.

Usage:
    python migrate_imap.py --src-host imap.source.com --src-user user@source.com --src-pass pass `
                           --dst-host imap.dest.com --dst-user user@dest.com --dst-pass pass

    python migrate_imap.py --config config.yaml
    python migrate_imap.py --dry-run
"""

import argparse
import getpass
import imaplib
import logging
import os
import re
import ssl
import sys
import time
from datetime import datetime

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
log = logging.getLogger("imap_migrate")

try:
    import yaml
    HAS_YAML = True
except ImportError:
    HAS_YAML = False

try:
    from tqdm import tqdm
    HAS_TQDM = True
except ImportError:
    HAS_TQDM = False


class Config:
    def __init__(self):
        self.src_host = None
        self.src_port = 993
        self.src_user = None
        self.src_pass = None
        self.src_ssl = True
        self.dst_host = None
        self.dst_port = 993
        self.dst_user = None
        self.dst_pass = None
        self.dst_ssl = True
        self.batch_size = 100
        self.delay = 0.0
        self.dry_run = False
        self.skip_folders = []
        self.skip_older_than_days = 0
        self.preserve_flags = True
        self.preserve_dates = True
        self.only_folders = []
        self.timeout = 60
        self.verify_cert = True

    @classmethod
    def from_args(cls, args):
        c = cls()
        c.src_host = args.src_host
        c.src_port = args.src_port or (993 if args.src_ssl else 143)
        c.src_user = args.src_user
        c.src_pass = args.src_pass
        c.src_ssl = args.src_ssl
        c.dst_host = args.dst_host
        c.dst_port = args.dst_port or (993 if args.dst_ssl else 143)
        c.dst_user = args.dst_user
        c.dst_pass = args.dst_pass
        c.dst_ssl = args.dst_ssl
        c.batch_size = args.batch_size
        c.delay = args.delay
        c.dry_run = args.dry_run
        c.skip_folders = args.skip_folders or []
        c.skip_older_than_days = args.skip_older_than_days
        c.preserve_flags = not args.no_flags
        c.preserve_dates = not args.no_dates
        c.only_folders = args.only_folders or []
        c.timeout = args.timeout
        c.verify_cert = not args.no_verify
        return c

    @classmethod
    def from_yaml(cls, path):
        if not HAS_YAML:
            log.error("PyYAML is required to use --config. Install it: pip install pyyaml")
            sys.exit(1)
        with open(path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)
        c = cls()
        sec = data.get("source", {})
        c.src_host = sec.get("host")
        c.src_port = sec.get("port", 993)
        c.src_user = sec.get("user")
        c.src_pass = sec.get("password")
        c.src_ssl = sec.get("ssl", True)
        sec = data.get("destination", {})
        c.dst_host = sec.get("host")
        c.dst_port = sec.get("port", 993)
        c.dst_user = sec.get("user")
        c.dst_pass = sec.get("password")
        c.dst_ssl = sec.get("ssl", True)
        opts = data.get("options", {})
        c.batch_size = opts.get("batch_size", 100)
        c.delay = opts.get("delay", 0.0)
        c.dry_run = opts.get("dry_run", False)
        c.skip_folders = opts.get("skip_folders", [])
        c.skip_older_than_days = opts.get("skip_older_than_days", 0)
        c.preserve_flags = opts.get("preserve_flags", True)
        c.preserve_dates = opts.get("preserve_dates", True)
        c.only_folders = opts.get("only_folders", [])
        c.timeout = opts.get("timeout", 60)
        c.verify_cert = opts.get("verify_cert", True)
        return c


def connect(host, port, user, password, use_ssl, timeout, verify_cert):
    log.info("Connecting to %s:%d as %s (SSL=%s)", host, port, user, use_ssl)
    if use_ssl:
        ctx = ssl.create_default_context()
        if not verify_cert:
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE
        client = imaplib.IMAP4_SSL(host, port, ssl_context=ctx, timeout=timeout)
    else:
        client = imaplib.IMAP4(host, port, timeout=timeout)
        client.starttls()
    try:
        r = client.login(user, password)
        log.debug("Login result: %s", r)
    except imaplib.IMAP4.error as e:
        log.error("Login failed for %s: %s", user, e)
        raise
    log.info("Connected successfully")
    return client


def list_folders(client):
    folders = []
    typ, data = client.list()
    if typ != "OK":
        log.error("Failed to list folders: %s", data)
        return folders
    pattern = re.compile(r'^\s*\((?P<flags>.*?)\)\s+"(?P<delim>[^"]*)"\s+"?(?P<name>.*?)"?\s*$')
    for line in data:
        if isinstance(line, bytes):
            line = line.decode("utf-8", errors="replace")
        m = pattern.match(line)
        if m:
            name = m.group("name")
            flags = m.group("flags")
            folders.append({"name": name, "flags": flags, "delim": m.group("delim")})
        else:
            folders.append({"name": line.strip(), "flags": "", "delim": "/"})
    return folders


def create_folder(client, folder_name):
    typ, data = client.create(folder_name)
    if typ == "OK":
        log.debug("Created folder: %s", folder_name)
        return True
    log.warning("Failed to create folder %s: %s", folder_name, data)
    return False


def folder_exists(client, folder_name):
    typ, data = client.list()
    if typ != "OK":
        return False
    for line in data:
        if isinstance(line, bytes):
            line = line.decode("utf-8", errors="replace")
        if '"' + folder_name + '"' in line or line.endswith('"' + folder_name + '"'):
            return True
        parts = line.rsplit('"', 2)
        if len(parts) >= 3 and parts[-2].strip('"') == folder_name:
            return True
    return False


def folder_matches(folder_name, patterns):
    if not patterns:
        return True
    for pattern in patterns:
        if re.search(pattern, folder_name, re.IGNORECASE):
            return True
    return False


def get_hierarchy(folders):
    tree = {}
    for f in folders:
        name = f["name"]
        delim = f["delim"] if f["delim"] else "/"
        parts = name.split(delim)
        current = tree
        for part in parts:
            if part not in current:
                current[part] = {}
            current = current[part]
    return tree


def print_hierarchy(tree, indent=0):
    for name, subtree in sorted(tree.items()):
        prefix = "  " * indent + "|-- " if indent > 0 else "|-- "
        print(f"{prefix}{name}")
        if subtree:
            print_hierarchy(subtree, indent + 1)


def migrate_folder(src, dst, src_folder, dst_folder, cfg, stats):
    log.info("Processing folder: %s -> %s", src_folder, dst_folder)

    r = src.select(src_folder, readonly=True)
    if r[0] != "OK":
        log.warning("Cannot select source folder %s: %s", src_folder, r[1])
        return
    msg_count = int(r[1][0])
    if msg_count == 0:
        log.info("  Folder is empty, skipping")
        src.close()
        return

    if cfg.dry_run:
        log.info("  [DRY-RUN] Would copy %d messages to %s", msg_count, dst_folder)
        src.close()
        stats["folders"] += 1
        stats["messages"] += msg_count
        return

    if not folder_exists(dst, dst_folder):
        create_folder(dst, dst_folder)

    # Ensure destination is selected
    r = dst.select(dst_folder, readonly=False)
    if r[0] != "OK":
        log.warning("Cannot select destination folder %s: %s", dst_folder, r[1])
        src.close()
        return

    total_copied = 0
    total_skipped = 0
    total_errors = 0

    # Get existing destination UIDs to avoid duplicates (via Message-ID heuristic)
    existing_ids = set()
    try:
        r_uid, data_uid = dst.uid("SEARCH", "ALL")
        if r_uid == "OK" and data_uid[0]:
            dst_uids = data_uid[0].split()
            for duid in dst_uids:
                typ_f, data_f = dst.uid("FETCH", duid, "(BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])")
                if typ_f == "OK" and data_f[0]:
                    raw = data_f[0]
                    if isinstance(raw, tuple):
                        raw = raw[1]
                    if isinstance(raw, bytes):
                        mid_match = re.search(rb'Message-ID:\s*(\S+)', raw, re.IGNORECASE)
                        if mid_match:
                            existing_ids.add(mid_match.group(1).lower())
    except Exception:
        pass

    # Re-select source
    src.select(src_folder, readonly=True)
    typ, data = src.uid("SEARCH", "ALL")
    if typ != "OK" or not data[0]:
        log.info("  No messages to copy")
        src.close()
        dst.close()
        return

    uids = data[0].split()
    log.info("  Messages to process: %d", len(uids))

    cutoff_date = None
    if cfg.skip_older_than_days > 0:
        cutoff_ts = time.time() - cfg.skip_older_than_days * 86400
        cutoff_date = datetime.fromtimestamp(cutoff_ts)

    iterator = tqdm(uids, desc=f"  {src_folder}", unit="msg") if HAS_TQDM else uids

    batch_uids = []
    batch_msgs = []
    batch_flags_list = []
    batch_dates = []

    def flush_batch():
        nonlocal total_copied, total_errors
        if not batch_uids:
            return
        dst.select(dst_folder, readonly=False)
        for i, raw_msg in enumerate(batch_msgs):
            try:
                flags = batch_flags_list[i]
                msg_date = batch_dates[i]

                flag_str = ""
                if cfg.preserve_flags and flags:
                    flag_str = " ".join(flags)

                r2, d2 = dst.append(
                    dst_folder,
                    flag_str.encode() if flag_str else None,
                    msg_date,
                    raw_msg,
                )
                if r2 == "OK":
                    total_copied += 1
                else:
                    total_errors += 1
                    log.warning("  Failed to append message (UID %s): %s", batch_uids[i], d2)
            except Exception as e:
                total_errors += 1
                log.warning("  Error appending message (UID %s): %s", batch_uids[i], e)

        if cfg.delay > 0:
            time.sleep(cfg.delay)

        batch_uids.clear()
        batch_msgs.clear()
        batch_flags_list.clear()
        batch_dates.clear()

    for uid in iterator:
        try:
            uid_str = uid.decode() if isinstance(uid, bytes) else uid

            # Fetch message data
            typ_f, data_f = src.uid("FETCH", uid_str, "(FLAGS INTERNALDATE RFC822)")
            if typ_f != "OK" or not data_f[0]:
                total_errors += 1
                continue

            raw_email = None
            flags = []
            internaldate = None
            msg_id = None

            for part in data_f:
                if isinstance(part, tuple) and len(part) >= 2:
                    raw_email = part[1]
                    raw_flags = imaplib.ParseFlags(part[0]) if part[0] else []
                    flags = [f.decode() if isinstance(f, bytes) else f for f in raw_flags]
                elif isinstance(part, bytes):
                    m = re.search(rb'INTERNALDATE\s+"([^"]*)"', part)
                    if m:
                        try:
                            dt = datetime.strptime(m.group(1).decode(), "%d-%b-%Y %H:%M:%S %z")
                            internaldate = dt
                        except ValueError:
                            pass

            if raw_email is None:
                for part in data_f:
                    if isinstance(part, tuple):
                        raw_email = part[1]
                        break

            if raw_email is None:
                total_errors += 1
                continue

            # Skip if duplicate by Message-ID
            mid_match = re.search(rb'Message-ID:\s*(\S+)', raw_email[:4096], re.IGNORECASE)
            if mid_match:
                msg_id = mid_match.group(1).lower()
                if msg_id in existing_ids:
                    total_skipped += 1
                    continue

            # Check date filter
            if cutoff_date and internaldate and internaldate < cutoff_date:
                total_skipped += 1
                continue

            msg_date = internaldate if (cfg.preserve_dates and internaldate) else None

            batch_uids.append(uid_str)
            batch_msgs.append(raw_email)
            batch_flags_list.append(flags)
            batch_dates.append(msg_date)

            if len(batch_uids) >= cfg.batch_size:
                flush_batch()

        except Exception as e:
            total_errors += 1
            log.warning("  Error processing UID %s: %s", uid, e)

    flush_batch()

    src.close()
    dst.close()

    stats["folders"] += 1
    stats["messages"] += total_copied
    stats["skipped"] += total_skipped
    stats["errors"] += total_errors
    log.info("  Done: %d copied, %d skipped, %d errors", total_copied, total_skipped, total_errors)


def parse_args(argv=None):
    parser = argparse.ArgumentParser(
        description="Migrate emails between IMAP servers preserving folder structure.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""Examples:
  migrate_imap.py --src-host imap.gmail.com --src-user me@gmail.com --dst-host imap.fastmail.com --dst-user me@fastmail.com
  migrate_imap.py --config config.yaml
  migrate_imap.py --dry-run --only-folders INBOX Sent
  Use --src-pass - or --dst-pass - to be prompted for the password.""",
    )

    g_src = parser.add_argument_group("Source server")
    g_src.add_argument("--src-host", help="Source IMAP server hostname")
    g_src.add_argument("--src-port", type=int, default=0, help="Source IMAP port (default: 993/143)")
    g_src.add_argument("--src-user", help="Source IMAP username")
    g_src.add_argument("--src-pass", help="Source IMAP password (use '-' to prompt)")
    g_src.add_argument("--src-ssl", action=argparse.BooleanOptionalAction, default=True, help="Use SSL for source")

    g_dst = parser.add_argument_group("Destination server")
    g_dst.add_argument("--dst-host", help="Destination IMAP server hostname")
    g_dst.add_argument("--dst-port", type=int, default=0, help="Destination IMAP port (default: 993/143)")
    g_dst.add_argument("--dst-user", help="Destination IMAP username")
    g_dst.add_argument("--dst-pass", help="Destination IMAP password (use '-' to prompt)")
    g_dst.add_argument("--dst-ssl", action=argparse.BooleanOptionalAction, default=True, help="Use SSL for destination")

    g_opt = parser.add_argument_group("Options")
    g_opt.add_argument("--config", help="Path to YAML config file")
    g_opt.add_argument("--batch-size", type=int, default=100, help="Messages per batch (default: 100)")
    g_opt.add_argument("--delay", type=float, default=0.0, help="Delay in seconds between batches")
    g_opt.add_argument("--dry-run", action="store_true", help="Preview only, no actual migration")
    g_opt.add_argument("--skip-folders", nargs="*", default=[], help="Folder name patterns to skip")
    g_opt.add_argument("--only-folders", nargs="*", default=[], help="Only migrate folders matching these patterns")
    g_opt.add_argument("--skip-older-than-days", type=int, default=0, help="Skip messages older than N days")
    g_opt.add_argument("--no-flags", action="store_true", help="Do not preserve message flags")
    g_opt.add_argument("--no-dates", action="store_true", help="Do not preserve internal dates")
    g_opt.add_argument("--timeout", type=int, default=60, help="IMAP connection timeout (default: 60)")
    g_opt.add_argument("--no-verify", action="store_true", help="Skip SSL certificate verification")
    g_opt.add_argument("--verbose", "-v", action="store_true", help="Detailed logging")
    g_opt.add_argument("--quiet", "-q", action="store_true", help="Minimal output")
    g_opt.add_argument("--show-folders", action="store_true", help="List folders on source server and exit")

    args = parser.parse_args(argv)
    return args


def main():
    args = parse_args()

    if args.verbose:
        log.setLevel(logging.DEBUG)
    elif args.quiet:
        log.setLevel(logging.WARNING)

    if args.config:
        if not os.path.exists(args.config):
            log.error("Config file not found: %s", args.config)
            sys.exit(1)
        cfg = Config.from_yaml(args.config)
    else:
        missing = []
        for a in ["src_host", "src_user", "dst_host", "dst_user"]:
            if not getattr(args, a):
                missing.append(a.replace("_", "-"))
        if missing:
            log.error("Missing required arguments: %s", ", ".join(missing))
            log.error("Use --help to see all options")
            sys.exit(1)

        if args.src_pass == "-":
            args.src_pass = getpass.getpass(f"Source password for {args.src_user}: ")
        if args.dst_pass == "-":
            args.dst_pass = getpass.getpass(f"Destination password for {args.dst_user}: ")

        cfg = Config.from_args(args)

    if not cfg.src_host or not cfg.src_user:
        log.error("Source host and user are required")
        sys.exit(1)
    if not cfg.dst_host or not cfg.dst_user:
        log.error("Destination host and user are required")
        sys.exit(1)

    # Connect to source
    try:
        src = connect(cfg.src_host, cfg.src_port, cfg.src_user, cfg.src_pass, cfg.src_ssl, cfg.timeout, cfg.verify_cert)
    except Exception as e:
        log.error("Failed to connect to source: %s", e)
        sys.exit(1)

    # List folders
    folders = list_folders(src)
    log.info("Found %d folders on source", len(folders))

    if args.show_folders:
        print("\nFolders on source server:")
        print("-" * 40)
        tree = get_hierarchy(folders)
        print_hierarchy(tree)
        src.logout()
        return

    # Filter folders
    if cfg.only_folders:
        folders = [f for f in folders if folder_matches(f["name"], cfg.only_folders)]
    if cfg.skip_folders:
        folders = [f for f in folders if not folder_matches(f["name"], cfg.skip_folders)]

    # Skip system/non-mail folders
    skip_system = [r"\[Gmail\]", r"\\Noselect", r"Spam", r"Junk", r"Trash", r"\[AOL", r"Bulk Mail"]
    folders = [f for f in folders if not folder_matches(f["name"], skip_system)]

    log.info("Folders to migrate (%d):", len(folders))
    tree = get_hierarchy(folders)
    print_hierarchy(tree)

    if cfg.dry_run:
        log.info("DRY RUN - no messages will be copied")

    # Connect to destination
    try:
        dst = connect(cfg.dst_host, cfg.dst_port, cfg.dst_user, cfg.dst_pass, cfg.dst_ssl, cfg.timeout, cfg.verify_cert)
    except Exception as e:
        log.error("Failed to connect to destination: %s", e)
        src.logout()
        sys.exit(1)

    # Migrate
    stats = {"folders": 0, "messages": 0, "skipped": 0, "errors": 0}
    start_time = time.time()

    for f in folders:
        folder_name = f["name"]
        migrate_folder(src, dst, folder_name, folder_name, cfg, stats)

    elapsed = time.time() - start_time

    src.logout()
    dst.logout()

    # Print summary
    print("\n" + "=" * 55)
    print("  MIGRATION SUMMARY")
    print("=" * 55)
    print(f"  Folders processed:   {stats['folders']}")
    print(f"  Messages copied:     {stats['messages']}")
    print(f"  Messages skipped:    {stats['skipped']}")
    print(f"  Errors:              {stats['errors']}")
    print(f"  Time elapsed:        {elapsed:.1f}s")
    if stats["messages"] > 0 and elapsed > 0:
        print(f"  Speed:               {stats['messages']/elapsed:.1f} msg/s")
    print("=" * 55)


if __name__ == "__main__":
    main()
