"""
inbox_full_sync v1.1
Název:  inbox_full_sync_v1.1.py
Verze:  1.1.0
Datum:  2026-06-08
Autor:  vladimir.buzalka

Popis:
    Jednorázový skript pro úplný přenos Inboxu A Sent Items z JNJ Outlooku (MAPI)
    do osobní schránky vladimir.buzalka@buzalka.cz přes Graph API.

    Prochází celý Inbox i Sent Items včetně všech podsložek. Online Archive se
    nepřenáší (GetDefaultFolder(5/6) vrátí pouze primární schránku).

    Každý email se uloží jako .msg do temp složky, odešle na https://msgs.buzalka.cz/upload
    a přes Graph API se importuje do odpovídající složky v osobní schránce.
    Dedup zajišťuje SQLite DB — email který je v DB (message_id) se přeskočí.

Spouštění:
    Spouštět ručně jako záchranná síť nebo iniciální sync.
    Bezpečné opakovat — duplicity se přeskočí.

Závislosti:
    win32com, requests, sqlite3 (stdlib)
    Python 3.10+, Windows, Outlook musí být spuštěn

Konfigurace (konstanty v kódu):
    TOKEN           Bearer token pro msgs.buzalka.cz
    UPLOAD_URL      https://msgs.buzalka.cz/upload
    DB_UPLOAD_URL   https://msgs.buzalka.cz/upload-db
    DB_PATH         C:\\Users\\vbuzalka\\SQLITE\\jnjemails.db
    LOG_PATH        C:\\Users\\vbuzalka\\SQLITE\\inbox_full_sync_errors.log

SQLite DB (jnjemails.db):
    messages    — přenesené emaily (message_id, entry_id, graph_id, is_read, jnj_folder, ...)
    runs        — jeden záznam na běh (script, version, started_at, finished_at, counts)
    log         — flat event log per run (level, event, subject, folder, graph_id, detail)

    Dotaz pro posledn běh:
        SELECT r.script, r.version, r.started_at, l.level, l.event,
               l.subject, l.folder, l.detail, l.created_at
        FROM log l JOIN runs r ON r.id = l.run_id
        WHERE l.run_id = (SELECT MAX(id) FROM runs)
        ORDER BY l.created_at

Log události (log.event):
    run_start       — start skriptu
    mailbox         — název schránky
    folder_start    — vstup do složky (detail = počet položek)
    folder_done     — konec složky (detail = přeneseno/skip)
    upload_saved    — nový email přenesen
    upload_exists   — email již v DB, přeskočen
    upload_error    — chyba při uploadu (detail = chybová zpráva)
    progress        — každých 100 přenesených
    db_upload       — úspěšný upload DB na server
    db_upload_error — chyba uploadu DB
    run_done        — konec skriptu (detail = souhrn)

Historie verzí:
    1.0.0  2026-06-01  Základní funkce: Inbox full scan, dedup přes DB, entry_id/graph_id/is_read
    1.0.1  2026-06-01  DB upload každých 100 emailů + finální upload
    1.0.2  2026-06-01  SQLite tabulky runs + log
    1.0.3  2026-06-01  Kompletní konzolový výstup zrcadlen do log tabulky, skipped counter
    1.0.4  2026-06-01  Šifrování Fernet (.emsg) pro bypass Zscaler DLP; rozšířený error detail
    1.1.0  2026-06-08  Synchronizace i složky Sent Items (GetDefaultFolder(5)) vedle Inboxu
"""
import win32com.client
import requests
import sqlite3
import urllib3
import logging
import hashlib
import base64
from pathlib import Path
from datetime import datetime
from cryptography.fernet import Fernet
import tempfile

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

TOKEN = "13e1bb01-9fd5-44a8-8ce9-4ee27133d340"
UPLOAD_URL = "https://msgs.buzalka.cz/upload"
DB_PATH = r"C:\Users\vbuzalka\SQLITE\jnjemails.db"
LOG_PATH = r"C:\Users\vbuzalka\SQLITE\inbox_full_sync_errors.log"
PR_INTERNET_MESSAGE_ID = "http://schemas.microsoft.com/mapi/proptag/0x1035001E"
DB_UPLOAD_URL = "https://msgs.buzalka.cz/upload-db"
SCRIPT_NAME = "inbox_full_sync"
SCRIPT_VERSION = "1.1.0"
# Výchozí složky k synchronizaci: (olFolderID, label) — Inbox=6, Sent Items=5
SYNC_FOLDERS = [(6, "Inbox"), (5, "Sent Items")]
# Šifrovací klíč odvozený z TOKENu — stejný algoritmus jako na serveru
_FERNET = Fernet(base64.urlsafe_b64encode(hashlib.sha256(TOKEN.encode()).digest()))

logging.basicConfig(
    filename=LOG_PATH,
    level=logging.ERROR,
    format="%(asctime)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    encoding="utf-8",
)


def init_db(conn):
    conn.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            message_id  TEXT NOT NULL,
            subject     TEXT,
            sender      TEXT,
            received_at TEXT,
            folder      TEXT,
            source      TEXT,
            uploaded_at TEXT DEFAULT (datetime('now')),
            entry_id    TEXT,
            graph_id    TEXT,
            is_read     INTEGER DEFAULT 0,
            jnj_folder  TEXT
        )
    """)
    conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_message_id ON messages(message_id)")

    conn.execute("""
        CREATE TABLE IF NOT EXISTS runs (
            id           INTEGER PRIMARY KEY AUTOINCREMENT,
            script       TEXT NOT NULL,
            version      TEXT,
            started_at   TEXT NOT NULL,
            finished_at  TEXT,
            transferred  INTEGER DEFAULT 0,
            skipped      INTEGER DEFAULT 0,
            sync_updated INTEGER DEFAULT 0,
            sync_deleted INTEGER DEFAULT 0,
            errors       INTEGER DEFAULT 0
        )
    """)

    conn.execute("""
        CREATE TABLE IF NOT EXISTS log (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            run_id     INTEGER REFERENCES runs(id),
            level      TEXT NOT NULL,
            event      TEXT NOT NULL,
            subject    TEXT,
            folder     TEXT,
            graph_id   TEXT,
            detail     TEXT,
            created_at TEXT DEFAULT (datetime('now'))
        )
    """)
    conn.execute("CREATE INDEX IF NOT EXISTS idx_log_run_id ON log(run_id)")

    for col, definition in [
        ("entry_id",   "TEXT"),
        ("graph_id",   "TEXT"),
        ("is_read",    "INTEGER DEFAULT 0"),
        ("jnj_folder", "TEXT"),
    ]:
        try:
            conn.execute(f"ALTER TABLE messages ADD COLUMN {col} {definition}")
        except Exception:
            pass
    conn.commit()


def start_run(conn):
    cur = conn.execute(
        "INSERT INTO runs (script, version, started_at) VALUES (?, ?, datetime('now'))",
        (SCRIPT_NAME, SCRIPT_VERSION)
    )
    conn.commit()
    return cur.lastrowid


def finish_run(conn, run_id, transferred, skipped, errors):
    conn.execute("""
        UPDATE runs SET finished_at=datetime('now'), transferred=?, skipped=?, errors=?
        WHERE id=?
    """, (transferred, skipped, errors, run_id))
    conn.commit()


def db_log(conn, run_id, level, event, subject=None, folder=None, graph_id=None, detail=None):
    conn.execute("""
        INSERT INTO log (run_id, level, event, subject, folder, graph_id, detail)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    """, (run_id, level, event, subject, folder, graph_id, detail))
    conn.commit()


def info(conn, run_id, event, **kwargs):
    db_log(conn, run_id, "INFO", event, **kwargs)


def error(conn, run_id, event, **kwargs):
    db_log(conn, run_id, "ERROR", event, **kwargs)


def is_uploaded(conn, message_id):
    row = conn.execute(
        "SELECT 1 FROM messages WHERE message_id = ? LIMIT 1", (message_id,)
    ).fetchone()
    return row is not None


def save_to_db(conn, message_id, subject, sender, received_at, folder,
               entry_id=None, graph_id=None, is_read=0):
    conn.execute("""
        INSERT OR IGNORE INTO messages
            (message_id, subject, sender, received_at, folder, source,
             entry_id, graph_id, is_read, jnj_folder)
        VALUES (?, ?, ?, ?, ?, 'inbox_full_sync', ?, ?, ?, ?)
    """, (message_id, subject, sender, received_at, folder,
          entry_id, graph_id, is_read, folder))
    conn.commit()


def upload_db(conn, run_id):
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"jnjemails_{timestamp}.db"
    try:
        with open(DB_PATH, "rb") as f:
            resp = requests.post(
                DB_UPLOAD_URL,
                headers={"Authorization": f"Bearer {TOKEN}"},
                files={"file": (filename, f, "application/octet-stream")},
                timeout=60,
            )
        result = resp.json()
        msg = f"DB upload: {result}"
        print(f"  {msg}")
        info(conn, run_id, "db_upload", detail=msg)
    except Exception as e:
        msg = str(e)
        print(f"  DB upload CHYBA: {msg}")
        error(conn, run_id, "db_upload_error", detail=msg)


def upload_msg(msg_path, filename, folder=""):
    size_kb = Path(msg_path).stat().st_size // 1024
    with open(msg_path, "rb") as f:
        encrypted = _FERNET.encrypt(f.read())
    enc_filename = Path(filename).stem + ".emsg"
    resp = requests.post(
        UPLOAD_URL,
        headers={"Authorization": f"Bearer {TOKEN}"},
        files={"file": (enc_filename, encrypted, "application/octet-stream")},
        data={"folder": folder},
        timeout=60,
    )
    if not resp.ok:
        raise requests.HTTPError(
            f"{resp.status_code} {resp.reason} | size={size_kb}KB | body={resp.text[:300]}",
            response=resp,
        )
    return resp.json()


def process_folder(conn, run_id, folder, folder_path, counter, skipped_counter, error_counter):
    current_path = f"{folder_path}/{folder.Name}"
    items = folder.Items
    items.Sort("[ReceivedTime]", False)

    count = 0
    skipped = 0
    total = items.Count

    msg = f"Složka: {current_path} ({total} položek)"
    print(f"\n  {msg}")
    info(conn, run_id, "folder_start", folder=current_path, detail=str(total))

    for item in items:
        subject = getattr(item, 'Subject', '?')
        try:
            if not item.MessageClass.upper().startswith("IPM.NOTE"):
                continue

            try:
                mid = item.PropertyAccessor.GetProperty(PR_INTERNET_MESSAGE_ID)
            except Exception:
                mid = None
            if not mid:
                mid = f"entryid:{item.EntryID}"

            if is_uploaded(conn, mid):
                skipped += 1
                skipped_counter[0] += 1
                continue

            try:
                with tempfile.TemporaryDirectory() as tmp:
                    safe_name = f"{item.EntryID[-20:]}.msg"
                    tmp_path = Path(tmp) / safe_name
                    item.SaveAs(str(tmp_path), 3)
                    size_kb = tmp_path.stat().st_size // 1024
                    result = upload_msg(tmp_path, safe_name, current_path)

                status = result.get("status", "?")
                graph_id = result.get("graph_id")
                is_read = 0 if item.UnRead else 1
                received = item.ReceivedTime.isoformat() if item.ReceivedTime else None

                save_to_db(conn, mid, subject, item.SenderEmailAddress,
                           received, current_path,
                           entry_id=item.EntryID, graph_id=graph_id, is_read=is_read)
                info(conn, run_id, f"upload_{status}",
                     subject=subject, folder=current_path, graph_id=graph_id,
                     detail=f"size={size_kb}KB")

                counter[0] += 1
                count += 1

                if counter[0] % 100 == 0:
                    msg = f"celkem přeneseno: {counter[0]}"
                    print(f"  → {msg}, uploaduji DB...")
                    info(conn, run_id, "progress", detail=msg)
                    upload_db(conn, run_id)

                print(f"    {status.upper():6} | {subject[:70]}")

            except Exception as e:
                sender_str = getattr(item, 'SenderEmailAddress', '?')
                received_str = getattr(item, 'ReceivedTime', None)
                received_str = received_str.isoformat() if received_str else '?'
                entry_id_str = getattr(item, 'EntryID', '?')
                detail = (
                    f"error={e} | "
                    f"sender={sender_str} | "
                    f"received={received_str} | "
                    f"entry_id={entry_id_str} | "
                    f"message_id={mid}"
                )
                print(f"    CHYBA | {subject[:50]} | sender={sender_str} | received={received_str} | {e}")
                error(conn, run_id, "upload_error",
                      subject=subject, folder=current_path, detail=detail)
                logging.error("folder=%s | %s", current_path, detail)
                error_counter[0] += 1

        except Exception as e:
            # Neočekávaná chyba mimo upload blok (MessageClass, EntryID, apod.)
            print(f"    CHYBA (item) | {subject[:50]} | {e}")
            logging.error("folder=%s | item_error | subject=%s | error=%s", current_path, subject, e)
            error_counter[0] += 1

    msg = f"složka hotova: přeneseno {count} | skip {skipped}"
    print(f"  → {msg}")
    info(conn, run_id, "folder_done", folder=current_path, detail=msg)

    for subfolder in folder.Folders:
        process_folder(conn, run_id, subfolder, current_path, counter, skipped_counter, error_counter)


# --- MAIN ---
print(f"=== inbox_full_sync v{SCRIPT_VERSION} ===")
print(f"Start: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

conn = sqlite3.connect(DB_PATH)
init_db(conn)
run_id = start_run(conn)
info(conn, run_id, "run_start", detail=f"script={SCRIPT_NAME} version={SCRIPT_VERSION}")

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

counter = [0]
skipped_counter = [0]
error_counter = [0]

for folder_id, folder_label in SYNC_FOLDERS:
    root = ns.GetDefaultFolder(folder_id)  # primární schránka, bez Online Archive
    mailbox_name = root.Parent.Name
    print(f"\nSchránka: {mailbox_name} | kořen: {folder_label}")
    info(conn, run_id, "mailbox", detail=f"{mailbox_name} | {folder_label}")
    process_folder(conn, run_id, root, f"/{mailbox_name}", counter, skipped_counter, error_counter)

finish_run(conn, run_id,
           transferred=counter[0],
           skipped=skipped_counter[0],
           errors=error_counter[0])

summary = f"přeneseno {counter[0]} | skip {skipped_counter[0]} | chyby {error_counter[0]}"
print(f"\n=== Hotovo: {summary} ===")
info(conn, run_id, "run_done", detail=summary)

print("Uploaduji DB...")
upload_db(conn, run_id)

print(f"Konec: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Chyby logovány do: {LOG_PATH}")
conn.close()
