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 hashlib
import json
import os
import re
import shutil


LOCAL_TZ = ZoneInfo("Europe/Berlin")
MARKER = ".generated_by_codex_wdr_mailfiles"
SKIP_ATTACHMENT_WORDS = (
    "agb",
    "bedingungen",
    "befoerderungsbedingungen",
    "beforderungsbedingungen",
    "beförderungsbedingungen",
    "tarif",
)


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="WDR-eTickets aus EML/MSG/Outlook in die Tabellen-Eingabestruktur extrahieren.")
    parser.add_argument("--work-dir", default=r"E:\codex\WDR-Ticket")
    parser.add_argument("--mail-dir", default="", help="Ordner mit .eml/.msg Dateien.")
    parser.add_argument("--outlook", action="store_true", help="Outlook per COM lesen.")
    parser.add_argument("--folder-filter", default="WDR", help="Outlook-Ordnerfilter; leer = alle Ordner.")
    return parser.parse_args(argv)


def clean_body(value: str) -> str:
    value = html_unescape(value or "")
    value = re.sub(r"<br\s*/?>", "\n", value, flags=re.I)
    value = re.sub(r"</p\s*>", "\n", value, flags=re.I)
    value = re.sub(r"<[^>]+>", " ", value)
    value = value.replace("\r\n", "\n").replace("\r", "\n")
    lines = [re.sub(r"\s+", " ", line).strip() for line in value.split("\n")]
    return "\n".join(line for line in lines if line).strip()


def booking_numbers(*values: str) -> list[str]:
    found: list[str] = []
    for value in values:
        text = value or ""
        for match in re.finditer(r"\b(?:Buchung|Buchungsnummer|Storno)\D{0,20}(\d{7,9})\b", text, re.I):
            if match.group(1) not in found:
                found.append(match.group(1))
        for match in re.finditer(r"ETICKET[_-](\d{7,9})(?:[_-]|\b)", text, re.I):
            if match.group(1) not in found:
                found.append(match.group(1))
    return found


def should_skip_attachment(filename: str) -> bool:
    low = filename.lower()
    return any(word in low for word in SKIP_ATTACHMENT_WORDS)


def sanitize(name: str, fallback: str) -> str:
    name = re.sub(r'[<>:"/\\|?*\x00-\x1f]+', "_", name or fallback).strip(" ._")
    return name[:140] or fallback


def unique_path(directory: Path, filename: str) -> Path:
    directory.mkdir(parents=True, exist_ok=True)
    safe = sanitize(Path(filename).name, "eticket.pdf")
    if not safe.lower().startswith("eticket"):
        safe = "eticket_" + safe
    target = directory / safe
    if not target.exists():
        return target
    for counter in range(2, 10000):
        candidate = directory / f"{target.stem}_{counter}{target.suffix}"
        if not candidate.exists():
            return candidate
    raise RuntimeError(f"Zu viele doppelte Dateinamen fuer {filename}")


def reset_output(out_root: Path) -> None:
    if out_root.exists():
        marker = out_root / MARKER
        if not marker.exists():
            raise RuntimeError(f"Aus Sicherheitsgruenden wird {out_root} nicht ueberschrieben: 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 rel(out_root: Path, path: Path) -> str:
    return path.relative_to(out_root).as_posix()


def dt_from_header(value: str) -> datetime:
    try:
        parsed = parsedate_to_datetime(value)
        if parsed.tzinfo is None:
            return parsed.replace(tzinfo=LOCAL_TZ)
        return parsed.astimezone(LOCAL_TZ)
    except Exception:
        return datetime.now(LOCAL_TZ)


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


def add_storno(out_root: Path, stornos: list[dict], subject: str, dt: datetime, body: str, source: str, index: int, raw: bytes | None = None) -> None:
    storno_dir = out_root / "stornos"
    stem = f"storno_{index:06d}"
    text_path, eml_path = write_text_pair(out_root, storno_dir, stem, body, raw)
    stornos.append({
        "subject": subject,
        "date_local": dt.isoformat(timespec="seconds"),
        "day": dt.strftime("%d.%m.%Y"),
        "time": f"{dt.hour}:{dt.minute:02d}",
        "source_mbox": source,
        "message_index": index,
        "booking_numbers": booking_numbers(subject),
        "text_href": rel(out_root, text_path),
        "eml_href": rel(out_root, eml_path) if eml_path else "",
        "body": body or "(Kein reiner Mailtext erkannt.)",
    })


def add_ticket(out_root: Path, tickets: list[dict], subject: str, dt: datetime, body: str, source: str, index: int, filename: str, payload: bytes, raw: bytes | None = None) -> None:
    if should_skip_attachment(filename):
        return
    pdf_dir = out_root / "pdfs"
    mail_dir = out_root / "mails"
    target = unique_path(pdf_dir, filename)
    target.write_bytes(payload)
    stem = f"{target.stem}_mail_{index:06d}"
    text_path, eml_path = write_text_pair(out_root, mail_dir, stem, body, raw)
    tickets.append({
        "pdf": rel(out_root, target),
        "pdf_name": target.name,
        "subject": subject,
        "date_local": dt.isoformat(timespec="seconds"),
        "day": dt.strftime("%d.%m.%Y"),
        "time": f"{dt.hour}:{dt.minute:02d}",
        "text_href": rel(out_root, text_path),
        "eml_href": rel(out_root, eml_path) if eml_path else "",
        "body": body or "(Kein reiner Mailtext erkannt.)",
        "from": "",
        "to": "",
        "source_mbox": source,
        "message_index": index,
        "booking_numbers": booking_numbers(subject, body, filename),
        "pdf_sha256": hashlib.sha256(payload).hexdigest(),
    })


def process_eml(path: Path, out_root: Path, tickets: list[dict], stornos: list[dict], index: int) -> None:
    raw = path.read_bytes()
    msg = BytesParser(policy=policy.default).parsebytes(raw)
    subject = str(msg.get("Subject") or "")
    dt = dt_from_header(str(msg.get("Date") or ""))
    body = ""
    for part in msg.walk():
        ctype = part.get_content_type()
        disp = (part.get_content_disposition() or "").lower()
        if disp != "attachment" and ctype in {"text/plain", "text/html"}:
            try:
                text = part.get_content()
            except Exception:
                continue
            body = body + "\n" + (clean_body(text) if ctype == "text/html" else clean_body(str(text)))
    body = body.strip()
    if "storno" in subject.lower():
        add_storno(out_root, stornos, subject, dt, body, str(path), index, raw)
    for part_no, part in enumerate(msg.iter_attachments(), 1):
        filename = part.get_filename() or f"anlage_{part_no}.pdf"
        if not filename.lower().endswith(".pdf"):
            continue
        payload = part.get_payload(decode=True) or b""
        if payload:
            add_ticket(out_root, tickets, subject, dt, body, str(path), index, filename, payload, raw)


def process_msg(path: Path, out_root: Path, tickets: list[dict], stornos: list[dict], index: int) -> None:
    try:
        import win32com.client
    except Exception as exc:
        raise RuntimeError("MSG-Import benoetigt Outlook/pywin32.") from exc
    outlook = win32com.client.Dispatch("Outlook.Application").Session
    item = outlook.OpenSharedItem(str(path))
    process_outlook_item(item, out_root, tickets, stornos, index, str(path))


def process_outlook_item(item, out_root: Path, tickets: list[dict], stornos: list[dict], index: int, source: str) -> None:
    subject = str(getattr(item, "Subject", "") or "")
    body = clean_body(str(getattr(item, "Body", "") or ""))
    received = getattr(item, "ReceivedTime", None)
    try:
        dt = datetime.fromtimestamp(received.timestamp(), LOCAL_TZ) if received else datetime.now(LOCAL_TZ)
    except Exception:
        dt = datetime.now(LOCAL_TZ)
    if "storno" in subject.lower():
        add_storno(out_root, stornos, subject, dt, body, source, index)
    attachments = getattr(item, "Attachments", None)
    if not attachments:
        return
    temp = out_root / "_tmp_msg"
    temp.mkdir(parents=True, exist_ok=True)
    for no in range(1, attachments.Count + 1):
        attachment = attachments.Item(no)
        filename = str(getattr(attachment, "FileName", "") or f"anlage_{no}.pdf")
        if not filename.lower().endswith(".pdf"):
            continue
        target = temp / sanitize(filename, f"anlage_{no}.pdf")
        attachment.SaveAsFile(str(target))
        payload = target.read_bytes()
        target.unlink(missing_ok=True)
        add_ticket(out_root, tickets, subject, dt, body, source, index, filename, payload)


def iter_outlook_items(folder, folder_filter: str, folder_path: str = ""):
    name = str(getattr(folder, "Name", "") or "")
    display_path = f"{folder_path}/{name}" if folder_path else name
    normalized_filter = (folder_filter or "").replace("\\", "/").lower().strip("/")
    normalized_path = display_path.replace("\\", "/").lower().strip("/")
    use_folder = not normalized_filter or normalized_filter in name.lower() or normalized_filter in normalized_path
    if use_folder:
        try:
            for idx in range(1, folder.Items.Count + 1):
                yield folder.Items.Item(idx), display_path
        except Exception:
            pass
    try:
        for no in range(1, folder.Folders.Count + 1):
            yield from iter_outlook_items(folder.Folders.Item(no), folder_filter, display_path)
    except Exception:
        pass


def process_outlook(out_root: Path, tickets: list[dict], stornos: list[dict], start_index: int, folder_filter: str) -> int:
    try:
        import win32com.client
    except Exception as exc:
        raise RuntimeError("Outlook-Import benoetigt installiertes Outlook und pywin32.") from exc
    outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
    index = start_index
    for store_no in range(1, outlook.Folders.Count + 1):
        root = outlook.Folders.Item(store_no)
        for item, folder_name in iter_outlook_items(root, folder_filter):
            index += 1
            try:
                process_outlook_item(item, out_root, tickets, stornos, index, f"Outlook:{folder_name}")
            except Exception:
                continue
    return index


def write_index(out_root: Path, tickets: list[dict], stornos: list[dict]) -> None:
    grouped: dict[str, list[dict]] = {}
    for item in stornos:
        for booking in item.get("booking_numbers", []):
            grouped.setdefault(booking, []).append(item)
    stornos_by_booking = {}
    for booking, items in grouped.items():
        body = "\n\n---\n\n".join(item.get("body", "") for item in items if item.get("body"))
        newest = max(items, key=lambda item: item.get("date_local", ""))
        stornos_by_booking[booking] = {
            "count": len(items),
            "day": newest.get("day", ""),
            "time": newest.get("time", ""),
            "href": newest.get("text_href", ""),
            "body": body,
            "subjects": " | ".join(item.get("subject", "") for item in items),
            "items": items,
        }
    index = {
        "created": datetime.now().isoformat(timespec="seconds"),
        "sources": [],
        "tickets": tickets,
        "stornos": stornos,
        "stornos_by_booking": stornos_by_booking,
        "counts": {
            "ticket_pdfs": len(tickets),
            "storno_messages": len(stornos),
            "storno_bookings": len(stornos_by_booking),
        },
    }
    (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")


def main(argv: list[str] | None = None) -> None:
    args = parse_args(argv)
    work_dir = Path(args.work_dir)
    out_root = work_dir / "etickets_aus_mbox"
    reset_output(out_root)
    tickets: list[dict] = []
    stornos: list[dict] = []
    index = 0
    if args.mail_dir:
        for path in sorted(Path(args.mail_dir).rglob("*")):
            if path.suffix.lower() not in {".eml", ".msg"}:
                continue
            index += 1
            if path.suffix.lower() == ".eml":
                process_eml(path, out_root, tickets, stornos, index)
            else:
                process_msg(path, out_root, tickets, stornos, index)
    if args.outlook:
        index = process_outlook(out_root, tickets, stornos, index, args.folder_filter)
    write_index(out_root, tickets, stornos)
    print(f"Mail-Datei/Outlook-Extraktion erstellt: {out_root}")
    print(f"Fahrkarten-PDFs: {len(tickets)}")
    print(f"Stornomails: {len(stornos)}")


if __name__ == "__main__":
    main()
