"""
mailbox_mirror v1.0 | 2026-06-08 | vladimir.buzalka

Zrcadlí primární JNJ schránku (BEZ Online Archive) za posledních 30 dní do
osobní schránky vladimir.buzalka@buzalka.cz.

Princip — bezestavový diff přes Internet Message-ID:
    1. Projdi Inbox(+podsložky), Sent, Deleted; vyber emaily z posledních 30 dní.
       Sestav manifest = [{message_id, folder, is_read}] (jen metadata, žádná těla).
    2. POST /mirror-plan → server porovná manifest se stavem schránky:
         - smaže ze schránky zprávy které v manifestu nejsou (smazané v JNJ)
         - vrátí to_add = message_id které ve schránce chybí
    3. Pro každé to_add: ulož .msg, zašifruj (Fernet → .emsg), POST /upload.

Žádná SQLite, žádný graph_id bookkeeping — zdrojem pravdy jsou obě schránky.
Mazání běží jen v rámci 30denního okna, starší archiv zůstává nedotčen.

Omezení JNJ:
    - Zscaler DLP → soubory se posílají šifrované (.emsg)
    - Online Archive vynechán (GetDefaultFolder vrací jen primární schránku)

Spouštění: opakovaně (Task Scheduler). Bezpečně opakovatelné a idempotentní.
Závislosti: pywin32, requests, cryptography. Outlook musí běžet.
"""
import sys
import base64
import hashlib
import tempfile
from pathlib import Path
from datetime import datetime, timedelta, timezone

import win32com.client
import requests
import urllib3
from cryptography.fernet import Fernet

sys.stdout.reconfigure(encoding="utf-8")
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

TOKEN       = "13e1bb01-9fd5-44a8-8ce9-4ee27133d340"
BASE_URL    = "https://msgs.buzalka.cz"
PLAN_URL    = f"{BASE_URL}/mirror-plan"
UPLOAD_URL  = f"{BASE_URL}/upload"
WINDOW_DAYS = 30

PR_INTERNET_MESSAGE_ID = "http://schemas.microsoft.com/mapi/proptag/0x1035001E"

# olFolderInbox=6, olFolderSentMail=5, olFolderDeletedItems=3
FOLDERS_TO_MIRROR = [6, 5, 3]

# Šifrovací klíč odvozený z TOKENu (stejný algoritmus jako server)
_FERNET = Fernet(base64.urlsafe_b64encode(hashlib.sha256(TOKEN.encode()).digest()))


def get_mid(item) -> str:
    try:
        mid = item.PropertyAccessor.GetProperty(PR_INTERNET_MESSAGE_ID)
    except Exception:
        mid = None
    return mid or f"entryid:{item.EntryID}"


def collect_manifest(ns, cutoff_local):
    """Projdi cílové složky + podsložky, vrať (manifest, index).

    manifest = [{message_id, folder, is_read}]
    index    = {message_id: (entry_id, folder_path)}  — pro fázi uploadu
    """
    restrict = (
        "@SQL=\"urn:schemas:httpmail:datereceived\" >= '%s'"
        % cutoff_local.strftime("%Y/%m/%d %H:%M:%S")
    )
    manifest = []
    index = {}

    def walk(folder, folder_path):
        current = f"{folder_path}/{folder.Name}"
        try:
            items = folder.Items.Restrict(restrict)
            items.Sort("[ReceivedTime]", False)
            n = 0
            for item in items:
                try:
                    if not item.MessageClass.upper().startswith("IPM.NOTE"):
                        continue
                    mid = get_mid(item)
                    manifest.append({
                        "message_id": mid,
                        "folder": current,
                        "is_read": (not item.UnRead),
                    })
                    index[mid] = (item.EntryID, current)
                    n += 1
                except Exception as e:
                    print(f"    chyba položky v {current}: {e}")
            print(f"  {current}: {n}")
        except Exception as e:
            print(f"  CHYBA složka {current}: {e}")
            return  # nedostupná složka → nelez do podsložek

        try:
            subfolders = list(folder.Folders)
        except Exception:
            subfolders = []
        for sub in subfolders:
            walk(sub, current)

    seen_roots = set()
    for fid in FOLDERS_TO_MIRROR:
        root = ns.GetDefaultFolder(fid)
        mailbox = root.Parent.Name
        key = (mailbox, root.Name)
        if key in seen_roots:
            continue
        seen_roots.add(key)
        walk(root, f"/{mailbox}")

    return manifest, index


def upload_one(ns, entry_id, folder):
    """Ulož email jako .msg, zašifruj a nahraj na /upload (server naimportuje)."""
    item = ns.GetItemFromID(entry_id)
    with tempfile.TemporaryDirectory() as tmp:
        safe_name = f"{entry_id[-20:]}.msg"
        tmp_path = Path(tmp) / safe_name
        item.SaveAs(str(tmp_path), 3)  # 3 = olMSG
        with open(tmp_path, "rb") as f:
            encrypted = _FERNET.encrypt(f.read())
    enc_name = safe_name[:-4] + ".emsg"
    resp = requests.post(
        UPLOAD_URL,
        headers={"Authorization": f"Bearer {TOKEN}"},
        files={"file": (enc_name, encrypted, "application/octet-stream")},
        data={"folder": folder},
        timeout=60,
    )
    resp.raise_for_status()
    return resp.json()


def main():
    print(f"=== mailbox_mirror v1.0 ===")
    print(f"Start: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

    cutoff_utc = datetime.now(timezone.utc) - timedelta(days=WINDOW_DAYS)
    cutoff_graph = cutoff_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
    cutoff_local = cutoff_utc.astimezone()
    print(f"Okno: posledních {WINDOW_DAYS} dní (cutoff {cutoff_graph})\n")

    outlook = win32com.client.Dispatch("Outlook.Application")
    ns = outlook.GetNamespace("MAPI")

    print("1) Sestavuji manifest z JNJ schránky...")
    manifest, index = collect_manifest(ns, cutoff_local)
    print(f"   → {len(manifest)} emailů v okně\n")

    print("2) Posílám plán na server (diff + mazání přebytků)...")
    resp = requests.post(
        PLAN_URL,
        headers={"Authorization": f"Bearer {TOKEN}"},
        json={"manifest": manifest, "cutoff": cutoff_graph},
        timeout=300,
    )
    resp.raise_for_status()
    plan = resp.json()
    to_add = plan.get("to_add", [])
    print(f"   schránka={plan.get('mailbox_count')} | manifest={plan.get('manifest_count')}")
    print(f"   smazáno ze schránky: {plan.get('deleted')}")
    print(f"   k nahrání: {len(to_add)}\n")

    if not to_add:
        print("Schránka je v synchronu, nic nenahrávám.")
    else:
        print("3) Nahrávám chybějící emaily...")
        uploaded = 0
        errors = 0
        for i, mid in enumerate(to_add, 1):
            entry_id, folder = index.get(mid, (None, None))
            if not entry_id:
                print(f"   [{i}/{len(to_add)}] chybí index pro {mid[:40]} — přeskočeno")
                errors += 1
                continue
            try:
                upload_one(ns, entry_id, folder)
                uploaded += 1
                if uploaded % 50 == 0:
                    print(f"   [{datetime.now().strftime('%H:%M:%S')}] "
                          f"nahráno {uploaded}/{len(to_add)}")
            except Exception as e:
                print(f"   CHYBA upload {mid[:40]}: {e}")
                errors += 1
        print(f"\n   nahráno {uploaded} | chyby {errors}")

    print(f"\n=== Hotovo === {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")


if __name__ == "__main__":
    main()
