from __future__ import annotations

from datetime import datetime
from email import policy
from email.parser import BytesParser
from email.utils import parsedate_to_datetime
from html import unescape as html_unescape
from pathlib import Path
from zoneinfo import ZoneInfo
import argparse
import csv
import hashlib
import json
import re
import shutil


WORK_DIR = Path(r"E:\codex\WDR-Ticket")
SOURCE_DIR = WORK_DIR / "thunderbird_sources"
OUT_ROOT = WORK_DIR / "etickets_aus_mbox"
LOCAL_TZ = ZoneInfo("Europe/Berlin")
MARKER = ".generated_by_codex_wdr_mbox"

SOURCE_COPY_NAMES = [
    "Archiv_Reisen_WDR.mbox",
    "IETimport_Orders_WDR.mbox",
    "IETimport_Orders_WYK_WDR.mbox",
]
LIVE_MBOX_FILES = [
    Path(r"E:\TB-Data\momj80ku.Jost Jahn\Mail\Local Folders\Archiv.sbd\!Reisen.sbd\WDR"),
    Path(r"E:\TB-Data\t5ttgr59.IETimport\Mail\Local Folders\Orders.sbd\Orders-O-Z.sbd\WDR"),
    Path(r"E:\TB-Data\t5ttgr59.IETimport\Mail\Local Folders\Orders.sbd\Orders-O-Z.sbd\wyk-wdr"),
]
MBOX_FILES = [SOURCE_DIR / name for name in SOURCE_COPY_NAMES]


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="WDR-Mails und eTicket-Anhaenge aus Thunderbird/MBOX extrahieren.")
    parser.add_argument("--work-dir", default=str(WORK_DIR), help="Arbeitsordner fuer die erzeugte Eingabestruktur.")
    parser.add_argument("--source-dir", default="", help="Ordner mit den kopierten MBOX-Dateien.")
    parser.add_argument("--mbox", action="append", default=[], help="MBOX-Datei; kann mehrfach angegeben werden.")
    return parser.parse_args(argv)


def configure_paths(args: argparse.Namespace) -> None:
    global WORK_DIR, SOURCE_DIR, OUT_ROOT, MBOX_FILES
    WORK_DIR = Path(args.work_dir)
    SOURCE_DIR = Path(args.source_dir) if args.source_dir else WORK_DIR / "thunderbird_sources"
    OUT_ROOT = WORK_DIR / "etickets_aus_mbox"
    if args.mbox:
        MBOX_FILES = [Path(item) for item in args.mbox]
    else:
        copy_candidates = [SOURCE_DIR / name for name in SOURCE_COPY_NAMES]
        selected = []
        for copy_path, live_path in zip(copy_candidates, LIVE_MBOX_FILES):
            if copy_path.exists():
                selected.append(copy_path)
            elif live_path.exists():
                selected.append(live_path)
        MBOX_FILES = selected or copy_candidates

SKIP_ATTACHMENT_WORDS = (
    "agb",
    "bedingungen",
    "befoerderungsbedingungen",
    "beforderungsbedingungen",
    "beförderungsbedingungen",
    "beförderung",
    "befoerderung",
    "tarifbestimmungen",
    "datenschutz",
)
TICKET_ATTACHMENT_WORDS = (
    "eticket",
    "ticket",
    "fahrkarte",
    "buchungsbest",
    "buchung",
)


def sanitize(value: str, fallback: str = "ohne_name", limit: int = 100) -> str:
    value = re.sub(r"[<>:\"/\\|?*\x00-\x1f]+", "_", value or "").strip(" ._")
    value = re.sub(r"\s+", "_", value)
    if not value:
        value = fallback
    return value[:limit]


def rel(path: Path) -> str:
    return path.relative_to(WORK_DIR).as_posix()


def reset_output() -> None:
    if OUT_ROOT.exists():
        marker = OUT_ROOT / MARKER
        if not marker.exists():
            raise RuntimeError(f"Aus Sicherheitsgründen wird {OUT_ROOT} nicht überschrieben: Marker fehlt.")
        shutil.rmtree(OUT_ROOT)
    OUT_ROOT.mkdir(parents=True, exist_ok=True)
    (OUT_ROOT / MARKER).write_text(datetime.now().isoformat(timespec="seconds"), encoding="utf-8")


def split_mbox_messages(data: bytes):
    starts = [0] if data.startswith(b"From ") else []
    starts.extend(match.start() + 1 for match in re.finditer(br"\nFrom ", data))
    for index, start in enumerate(starts):
        end = starts[index + 1] - 1 if index + 1 < len(starts) else len(data)
        raw = data[start:end]
        if raw.startswith(b"From "):
            raw = raw.partition(b"\n")[2]
        if raw.strip():
            yield raw


def local_dt(msg) -> datetime | None:
    raw = msg.get("Date")
    if not raw:
        return None
    try:
        dt = parsedate_to_datetime(raw)
    except Exception:
        return None
    if dt.tzinfo is None:
        return dt.replace(tzinfo=LOCAL_TZ)
    return dt.astimezone(LOCAL_TZ)


def clean_text(value: str) -> str:
    value = value.replace("\r\n", "\n").replace("\r", "\n")
    value = "\n".join(line.rstrip() for line in value.split("\n")).strip()
    return re.sub(r"\n{4,}", "\n\n\n", value)


def html_to_text(value: str) -> str:
    value = re.sub(r"(?is)<(script|style).*?</\1>", " ", value)
    value = re.sub(r"(?i)<br\s*/?>", "\n", value)
    value = re.sub(r"(?i)</(?:p|div|tr|li|h[1-6])\s*>", "\n", value)
    value = re.sub(r"(?s)<[^>]+>", " ", value)
    value = html_unescape(value)
    value = re.sub(r"[ \t]+", " ", value)
    value = re.sub(r" *\n *", "\n", value)
    return clean_text(value)


def decode_text_part(part) -> str:
    raw = part.get_payload(decode=True)
    if not isinstance(raw, bytes):
        try:
            content = part.get_content()
            return content if isinstance(content, str) else ""
        except Exception:
            return ""
    encodings: list[str] = []
    declared = part.get_content_charset()
    for encoding in (declared, "utf-8", "cp1252", "iso-8859-1"):
        if encoding and encoding.lower() not in {item.lower() for item in encodings}:
            encodings.append(encoding)
    candidates: list[tuple[int, int, str]] = []
    for priority, encoding in enumerate(encodings):
        try:
            text = raw.decode(encoding, errors="strict")
        except (LookupError, UnicodeDecodeError):
            continue
        damage = text.count("\ufffd") * 1000 + sum(text.count(token) for token in ("Ã", "Â", "â€")) * 20
        controls = sum(1 for char in text if ord(char) < 32 and char not in "\r\n\t")
        candidates.append((damage + controls, priority, text))
    if candidates:
        return min(candidates, key=lambda item: (item[0], item[1]))[2]
    return raw.decode(declared or "utf-8", errors="replace")


def mail_body(msg) -> str:
    plain_parts: list[str] = []
    html_parts: list[str] = []
    for part in msg.walk():
        if part.is_multipart():
            continue
        disposition = (part.get_content_disposition() or "").lower()
        if disposition == "attachment" or part.get_filename():
            continue
        content = decode_text_part(part)
        if not content:
            continue
        content_type = part.get_content_type().lower()
        if content_type == "text/plain":
            plain_parts.append(content)
        elif content_type == "text/html":
            html_parts.append(content)
    text = clean_text("\n\n".join(plain_parts))
    if not text and html_parts:
        text = html_to_text(html_parts[0])
    return text


def booking_numbers(*values: str) -> list[str]:
    text = "\n".join(value or "" for value in values)
    found = set(re.findall(r"\b\d{7,9}\b", text))
    return sorted(found)


def is_storno_subject(subject: str) -> bool:
    return "storno" in (subject or "").lower()


def is_ticket_attachment(filename: str) -> bool:
    lower = filename.lower()
    if not lower.endswith(".pdf"):
        return False
    if any(word in lower for word in SKIP_ATTACHMENT_WORDS):
        return False
    return any(word in lower for word in TICKET_ATTACHMENT_WORDS)


def unique_path(directory: Path, filename: str) -> Path:
    directory.mkdir(parents=True, exist_ok=True)
    safe = sanitize(Path(filename).name, "eticket.pdf", 130)
    if not safe.lower().startswith("eticket"):
        safe = "eticket_" + safe
    target = directory / safe
    if not target.exists():
        return target
    stem = target.stem
    suffix = target.suffix
    counter = 2
    while True:
        candidate = directory / f"{stem}_{counter}{suffix}"
        if not candidate.exists():
            return candidate
        counter += 1


def write_text_pair(base: Path, stem: str, body: str, raw: bytes) -> tuple[Path, Path]:
    base.mkdir(parents=True, exist_ok=True)
    txt = base / f"{stem}.txt"
    eml = base / f"{stem}.eml"
    txt.write_text(body or "(Kein reiner Mailtext erkannt.)", encoding="utf-8")
    eml.write_bytes(raw)
    return txt, eml


def main(argv: list[str] | None = None) -> None:
    args = parse_args(argv)
    configure_paths(args)
    reset_output()
    pdf_dir = OUT_ROOT / "pdfs"
    mail_dir = OUT_ROOT / "mails"
    storno_dir = OUT_ROOT / "stornos"
    storno_booking_dir = OUT_ROOT / "stornos_je_buchung"

    tickets: list[dict] = []
    storno_items: list[dict] = []
    manifest_rows: list[dict] = []
    source_stats: list[dict] = []
    seen_ticket_keys: set[tuple[str, str, str]] = set()
    duplicate_ticket_attachments = 0
    global_index = 0

    for mbox in MBOX_FILES:
        if not mbox.exists():
            raise FileNotFoundError(f"MBOX-Datei fehlt: {mbox}")
        data = mbox.read_bytes()
        source_count = 0
        ticket_count = 0
        storno_count = 0
        for raw in split_mbox_messages(data):
            source_count += 1
            global_index += 1
            try:
                msg = BytesParser(policy=policy.default).parsebytes(raw)
            except Exception as exc:
                manifest_rows.append({
                    "category": "parse_fehler",
                    "source_mbox": mbox.name,
                    "message_index": global_index,
                    "subject": "",
                    "date_local": "",
                    "booking_numbers": "",
                    "pdf": "",
                    "mail_text": "",
                    "storno_text": "",
                    "error": str(exc),
                })
                continue

            subject = str(msg.get("Subject", "") or "")
            body = mail_body(msg)
            dt = local_dt(msg)
            dt_iso = dt.isoformat(timespec="seconds") if dt else ""
            day = dt.strftime("%d.%m.%Y") if dt else ""
            time_value = f"{dt.hour}:{dt.minute:02d}" if dt else ""
            message_id = str(msg.get("Message-ID", "") or "")
            from_value = str(msg.get("From", "") or "")
            to_value = str(msg.get("To", "") or "")
            raw_hash = hashlib.sha256(raw).hexdigest()

            storno = is_storno_subject(subject)
            storno_bookings = booking_numbers(subject) if storno else []
            saved_ticket_names: list[str] = []

            if storno:
                storno_count += 1
                booking_part = storno_bookings[0] if storno_bookings else "ohne_nummer"
                stem = f"{global_index:06d}_{booking_part}_{sanitize(subject, 'storno', 60)}"
                text_path, eml_path = write_text_pair(storno_dir, stem, body, raw)
                item = {
                    "subject": subject,
                    "date_iso": dt_iso,
                    "day": day,
                    "time": time_value,
                    "message_id": message_id,
                    "from": from_value,
                    "to": to_value,
                    "source_mbox": mbox.name,
                    "message_index": global_index,
                    "booking_numbers": storno_bookings,
                    "text_href": rel(text_path),
                    "eml_href": rel(eml_path),
                    "body": body or "(Kein reiner Mailtext erkannt.)",
                    "raw_sha256": raw_hash,
                }
                storno_items.append(item)

            for part_index, part in enumerate(msg.walk(), start=1):
                filename = part.get_filename()
                if not filename or not is_ticket_attachment(filename):
                    continue
                payload = part.get_payload(decode=True)
                if not payload:
                    continue
                payload_hash = hashlib.sha256(payload).hexdigest()
                duplicate_key = (dt_iso, payload_hash)
                if duplicate_key in seen_ticket_keys:
                    duplicate_ticket_attachments += 1
                    manifest_rows.append({
                        "category": "dublette_fahrkarte",
                        "source_mbox": mbox.name,
                        "message_index": global_index,
                        "subject": subject,
                        "date_local": dt_iso,
                        "booking_numbers": ",".join(booking_numbers(subject, body, filename)),
                        "pdf": "",
                        "mail_text": "",
                        "storno_text": "",
                        "error": "Identischer PDF-Anhang mit gleichem Zustelldatum wurde bereits übernommen.",
                    })
                    continue
                seen_ticket_keys.add(duplicate_key)
                target = unique_path(pdf_dir, filename)
                target.write_bytes(payload)
                saved_ticket_names.append(target.name)
                ticket_count += 1
                ticket_stem = target.stem
                mail_stem = f"{ticket_stem}_mail_{global_index:06d}_{part_index:02d}"
                text_path, eml_path = write_text_pair(mail_dir, mail_stem, body, raw)
                ticket_bookings = booking_numbers(subject, body, filename)
                ticket = {
                    "pdf_name": target.name,
                    "pdf_href": rel(target),
                    "original_filename": filename,
                    "mail_day": day,
                    "mail_time": time_value,
                    "mail_date_iso": dt_iso,
                    "mail_subject": subject,
                    "mail_text_href": rel(text_path),
                    "mail_eml_href": rel(eml_path),
                    "mail_body": body or "(Kein reiner Mailtext erkannt.)",
                    "message_id": message_id,
                    "from": from_value,
                    "to": to_value,
                    "source_mbox": mbox.name,
                    "message_index": global_index,
                    "booking_numbers": ticket_bookings,
                    "raw_sha256": raw_hash,
                    "pdf_sha256": payload_hash,
                }
                tickets.append(ticket)
                manifest_rows.append({
                    "category": "fahrkarte",
                    "source_mbox": mbox.name,
                    "message_index": global_index,
                    "subject": subject,
                    "date_local": dt_iso,
                    "booking_numbers": ",".join(ticket_bookings),
                    "pdf": rel(target),
                    "mail_text": rel(text_path),
                    "storno_text": "",
                    "error": "",
                })

            if storno:
                manifest_rows.append({
                    "category": "storno",
                    "source_mbox": mbox.name,
                    "message_index": global_index,
                    "subject": subject,
                    "date_local": dt_iso,
                    "booking_numbers": ",".join(storno_bookings),
                    "pdf": ",".join(saved_ticket_names),
                    "mail_text": "",
                    "storno_text": storno_items[-1]["text_href"],
                    "error": "",
                })

        source_stats.append({
            "source": str(mbox),
            "messages": source_count,
            "ticket_attachments": ticket_count,
            "storno_messages": storno_count,
        })

    grouped: dict[str, list[dict]] = {}
    for item in storno_items:
        for booking in item.get("booking_numbers", []):
            grouped.setdefault(booking, []).append(item)

    stornos_by_booking: dict[str, dict] = {}
    for booking, items in sorted(grouped.items()):
        sorted_items = sorted(items, key=lambda x: x.get("date_iso") or "")
        parts = []
        for item in sorted_items:
            header = f"{item.get('day', '')} {item.get('time', '')} - {item.get('subject', '')}".strip()
            parts.append(header + "\n\n" + (item.get("body") or "(Kein reiner Mailtext erkannt.)"))
        body = "\n\n---\n\n".join(parts)
        text_path = storno_booking_dir / f"storno_{booking}.txt"
        text_path.parent.mkdir(parents=True, exist_ok=True)
        text_path.write_text(body, encoding="utf-8")
        latest = sorted_items[-1] if sorted_items else {}
        stornos_by_booking[booking] = {
            "count": len(items),
            "day": latest.get("day", ""),
            "time": latest.get("time", ""),
            "href": rel(text_path),
            "body": body,
            "subjects": "; ".join(item.get("subject", "") for item in items),
            "dates": "; ".join((item.get("day", "") + " " + item.get("time", "")).strip() for item in items),
            "items": [
                {
                    "subject": item.get("subject", ""),
                    "day": item.get("day", ""),
                    "time": item.get("time", ""),
                    "text_href": item.get("text_href", ""),
                    "eml_href": item.get("eml_href", ""),
                    "source_mbox": item.get("source_mbox", ""),
                    "message_index": item.get("message_index", ""),
                }
                for item in items
            ],
        }

    manifest_path = OUT_ROOT / "manifest.csv"
    with manifest_path.open("w", newline="", encoding="utf-8-sig") as handle:
        writer = csv.DictWriter(
            handle,
            fieldnames=[
                "category",
                "source_mbox",
                "message_index",
                "subject",
                "date_local",
                "booking_numbers",
                "pdf",
                "mail_text",
                "storno_text",
                "error",
            ],
            delimiter=";",
        )
        writer.writeheader()
        writer.writerows(manifest_rows)

    index = {
        "created": datetime.now().isoformat(timespec="seconds"),
        "sources": source_stats,
        "tickets": tickets,
        "stornos": storno_items,
        "stornos_by_booking": stornos_by_booking,
        "counts": {
            "ticket_pdfs": len(tickets),
            "duplicate_ticket_attachments_skipped": duplicate_ticket_attachments,
            "storno_messages": len(storno_items),
            "storno_bookings": len(stornos_by_booking),
            "manifest_rows": len(manifest_rows),
        },
    }
    (OUT_ROOT / "index.json").write_text(json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8")
    (OUT_ROOT / "summary.json").write_text(json.dumps(index["counts"] | {"created": index["created"]}, ensure_ascii=False, indent=2), encoding="utf-8")
    (OUT_ROOT / "README.txt").write_text(
        "Aus drei Thunderbird-MBOX-Dateien konstruierte Eingabestruktur für die WDR-eTicket-Tabelle.\n"
        "pdfs: nur erkannte Fahrkarten-PDFs; AGB/Beförderungsbedingungen wurden ausgelassen.\n"
        "mails: zu jeder Fahrkarte der zugehörige Mailtext und die Original-EML.\n"
        "stornos: Storno-Mails; Storno wird ausschließlich über 'Storno' im Betreff erkannt.\n"
        "stornos_je_buchung: zusammengefasste Storno-Texte pro Nummer aus dem Betreff.\n"
        "index.json verbindet PDFs, Mailtexte und Stornos für die Tabellenerzeugung.\n",
        encoding="utf-8",
    )

    print("Eingabeverzeichnis:", OUT_ROOT)
    print("PDF-Fahrkarten:", len(tickets))
    print("Übersprungene identische PDF-Dubletten:", duplicate_ticket_attachments)
    print("Storno-Mails:", len(storno_items))
    print("Storno-Buchungen aus Betreff:", len(stornos_by_booking))
    print("Manifest:", manifest_path)


if __name__ == "__main__":
    main()
