#!/usr/bin/env python3
"""
Puls-Backend Sync v2: liest Inbox + Sent Items + Stepstone-Bestätigungen.

Erkennt folgende Quellen als offene Bewerbungen:
  - Stepstone "Chris, deine Bewerbung als X ist raus" / "zugestellt"
  - Empfangsbestätigungen ("Eingang", "bestätigen", "erhalten")
  - Direktmails mit "Bewerbung" + konkreter Firma
  - Eigene Sent-Mails mit "Bewerbung" + Empfänger-Firma

Output: JSON mit den neu erkannten Events.
"""
from __future__ import annotations

import json
import os
import re
import subprocess
import sys
import urllib.request
import urllib.error
from datetime import datetime
from pathlib import Path

HOME = Path(os.environ.get("USERPROFILE") or r"C:\Users\server")
os.environ["PATH"] = (
    str(HOME / ".local" / "bin") + os.pathsep +
    os.environ.get("PATH", "")
)
HIMALAYA = HOME / ".local" / "bin" / "himalaya.exe"
BACKEND = "http://127.0.0.1:8000"
STATE_FILE = HOME / ".hermes" / "scripts" / "puls_state.json"

sys.path.insert(0, str(HOME / ".hermes" / "scripts"))
from parse_himalaya_envelope import parse_envelope_table, fetch_full_subject  # noqa: E402


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def list_mailbox(mailbox: str, page: int, page_size: int = 50):
    """Holt Envelopes aus beliebigem Mailbox (Sent/Sent Items/Inbox)."""
    result = subprocess.run(
        [str(HIMALAYA), "envelope", "list", "-m", mailbox,
         "--page", str(page), "--page-size", str(page_size),
         "--max-width", "200"],
        capture_output=True, text=True, timeout=30,
    )
    if result.returncode != 0:
        return []
    envs = parse_envelope_table(result.stdout)
    for e in envs:
        if len(e["subject"]) >= 85:
            full = fetch_full_subject(e["id"])
            if full:
                e["subject"] = full
    return envs


def fetch_body(env_id: str, max_chars: int = 4000) -> str:
    try:
        result = subprocess.run(
            [str(HIMALAYA), "message", "read", env_id],
            capture_output=True, text=True, timeout=20,
        )
        if result.returncode != 0:
            return ""
        body_start = result.stdout.find("\n\n")
        body = result.stdout[body_start + 2:] if body_start != -1 else result.stdout
        body = re.sub(r"=\r?\n", "", body)
        body = re.sub(r"=([0-9A-Fa-f]{2})", lambda m: chr(int(m.group(1), 16)), body)
        return body[:max_chars]
    except Exception:
        return ""


TO_RE = re.compile(r"^To:\s*(.+)$", re.IGNORECASE | re.MULTILINE)


def parse_address(raw: str) -> tuple[str, str]:
    raw = raw.strip()
    m = re.match(r"(.+?)\s*<([^>]+)>", raw)
    if m:
        return m.group(1).strip().strip('"'), m.group(2).strip()
    if "@" in raw:
        return "", raw.strip()
    return raw, ""


def extract_company_from_email(email: str) -> str | None:
    if not email or "@" not in email:
        return None
    domain = email.split("@", 1)[1].lower()
    parts = domain.split(".")
    if len(parts) < 2:
        return None
    generic = ("www", "mail", "smtp", "info", "contact", "office", "personal",
               "bewerbung", "bewerbungen", "jobs", "karriere", "career", "hr",
               "recruiting", "apply", "application", "no-reply", "noreply",
               "reply", "support", "kontakt", "postmaster", "donotreply")
    for p in parts[:-1]:
        if p not in generic:
            return p.capitalize()
    return parts[-2].capitalize()


def backend_get(path):
    with urllib.request.urlopen(BACKEND + path, timeout=10) as r:
        return json.loads(r.read().decode())


def backend_post(path, payload):
    req = urllib.request.Request(BACKEND + path,
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"}, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return r.status, json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        return e.code, {"error": e.read().decode("utf-8", errors="replace")[:200]}


def backend_put(path, payload):
    req = urllib.request.Request(BACKEND + path,
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"}, method="PUT")
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return r.status, json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        return e.code, {"error": e.read().decode("utf-8", errors="replace")[:200]}


# ---------------------------------------------------------------------------
# Classification
# ---------------------------------------------------------------------------
ABSAGE_KW = [
    "absage", "nicht weiter", "nicht für dich entschieden", "keine stelle",
    "abgelehnt", "leider mitteilen", "müssen wir ihnen heute mitteilen",
    "haben uns für einen anderen bew", "passt aktuell nicht",
    "haben uns für einen anderen kandidaten",
]
INTERVIEW_KW = ["interview", "einladung", "vorstellungsgespräch", "kennenlernen", "gesprächstermin", "entscheidung zu"]
EMPFANG_KW = ["eingang", "bestätig", "erhalten", "ist raus", "zugestellt", "vielen dank für ihre bewerbung"]
OFFER_KW = ["angebot", "vertragsentwurf", "offer"]
STATUS_KW = ["news zu deiner bewerbung", "zwischeninfo", "verzöger", "aktuelle status", "update zu deiner bewerbung"]


def classify(subject: str, body: str = "") -> tuple[str, str]:
    text = (subject + " " + body).lower()
    if any(k in text for k in ABSAGE_KW):
        return "rejected", "ABSAGE"
    if any(k in text for k in INTERVIEW_KW):
        return "interview", "EINLADUNG"
    if any(k in text for k in OFFER_KW):
        return "offer", "ANGEBOT"
    if any(k in text for k in STATUS_KW):
        return "open", "STATUS-UPDATE"
    if any(k in text for k in EMPFANG_KW):
        return "open", "EINGANGSBESTÄTIGUNG"
    return "open", "BEWERBUNGS-MAIL"


# ---------------------------------------------------------------------------
# Firm-Matching
# ---------------------------------------------------------------------------
NEWSLETTER_SENDER = (
    "linkedin jobbenachrichtigungen", "lisa stein von stepstone",
    "noreply@mlp.de", "1&1 kundenservice", "apollo",
    "christ - dein onlineshop", "nitrado", "louis motorrad",
    "ebay", "paypal", "microfiches", "hohenstraeter",
)


def is_newsletter(sender: str, subject: str) -> bool:
    hay = sender.lower()
    return any(n in hay for n in NEWSLETTER_SENDER)


# Stepstone-Bestätigung: "Chris, deine Bewerbung als X ist raus"
STEPSTONE_RE = re.compile(r"bewerbung als\s+(.+?)\s+ist raus", re.IGNORECASE)

# Workwise / Plattform-Bestätigungen
PLATFORM_RAUS = (
    "ist raus", "zugestellt", "versendet", "ihre bewerbung",
    "ihre bewerbung als", "ihre bewerbung beim",
)

# Konkrete Firmen-Erkennung in Empfangsbestätigungen (From-Spalte)
DIRECT_FIRM_FROM = {
    "spie": "SPIE",
    "secova": "secova",
    "validdata": "validdata",
    "vossko": "VOSSKO",
    "nicetec": "nicetec",
    "produktundmarkt": "Produkt + Markt",
    "produkt + markt": "Produkt + Markt",
    "company administration": "Produkt + Markt",
    "niedersachsen": "Land Niedersachsen",
    "workwise": "Workwise (Plattform)",
    "adi solutions": "ADI Solutions",
    "emvion": "emvion",
    "karriere@ausbildungsstellen": "ausbildungsstellen.de",
}


def extract_firm_from_subject_stepstone(subject: str) -> tuple[str | None, str | None]:
    """Stepstone: 'deine Bewerbung als X ist raus' — X ist die Position,
    Firma steckt in der Mail (Body). Wir geben erstmal nur Position zurück."""
    m = STEPSTONE_RE.search(subject)
    if m:
        return None, m.group(1).strip()
    return None, None


def extract_firm_from_subject_direct(subject: str, body: str) -> str | None:
    """Bei direkter Empfangsbestätigung: 'Ihre Bewerbung als X bei <Firma>'
    oder 'Deine Bewerbung bei <Firma>'."""
    # "Deine Bewerbung bei X"
    m = re.search(r"bewerbung bei\s+([A-ZÄÖÜ][A-Za-zäöüÄÖÜß&]+(?:\s+[A-ZÄÖÜ][A-Za-zäöüÄÖÜß&]+){0,4})", subject)
    if m:
        return m.group(1).strip()
    # "Ihre Bewerbung beim X"
    m = re.search(r"bewerbung beim\s+([A-ZÄÖÜ][A-Za-zäöüÄÖÜß&]+(?:\s+[A-ZÄÖÜ][A-Za-zäöüÄÖÜß&]+){0,4})", subject)
    if m:
        return m.group(1).strip()
    # "Bewerbung bei X (12345-abc)"
    m = re.search(r"stelle\s+([A-Za-zäöüÄÖÜß][^()]+?)\s+\(", subject)
    if m:
        return None  # nur Position
    # Aus From-Adresse
    return None


def extract_firm_from_from_address(sender: str) -> str | None:
    """Aus 'Glose,Barbara <glose@emvion.de>' -> emvion."""
    # E-Mail extrahieren
    m = re.search(r"<([^>]+@[^>]+)>", sender)
    if not m:
        m = re.search(r"([\w.+-]+@[\w.-]+\.[A-Za-z]{2,})", sender)
    if not m:
        return None
    return extract_company_from_email(m.group(1))


def extract_firm_from_body(body: str) -> str | None:
    """Aus dem Mail-Body die Firma extrahieren."""
    # "Vielen Dank für Ihre Bewerbung als X bei <Firma>."
    m = re.search(r"bewerbung als\s+[A-Za-zäöüÄÖÜß\s]+?bei\s+([A-ZÄÖÜ][A-Za-zäöüÄÖÜß&.\-]+(?:\s+[A-ZÄÖÜ][A-Za-zäöüÄÖÜß&.\-]+){0,3})", body, re.IGNORECASE)
    if m:
        return m.group(1).strip()
    return None


def position_from_subject(subject: str) -> str:
    """Aus Subject die Position ziehen."""
    m = re.search(r"bewerbung als\s+(.+?)(?:\s*\(|,|\s+ist raus|\s+zugestellt|\s*$)", subject, re.IGNORECASE)
    if m:
        return m.group(1).strip()
    m = re.search(r"stelle\s+([A-ZÄÖÜ][A-Za-zäöüÄÖÜß][^()]+?)\s+\(", subject)
    if m:
        return m.group(1).strip()
    m = re.search(r"bewerbung\s+([A-ZÄÖÜ][A-Za-zäöüÄÖÜß][^()]+?)(?:\s*\(|,|\s*$)", subject, re.IGNORECASE)
    if m:
        return m.group(1).strip()
    return "unbekannt"


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
    state = json.loads(STATE_FILE.read_text(encoding="utf-8")) if STATE_FILE.exists() else {"seen_env_ids": []}
    seen = set(state.get("seen_env_ids", []))
    new_events: list[dict] = []
    memory_db: dict[tuple[str, str], int] = {}

    # Sammle aus 3 Quellen
    inbox_envs = list_mailbox("Inbox", page=1, page_size=200)
    sent_items_envs = list_mailbox("Sent Items", page=1, page_size=50)

    print(f"Inbox: {len(inbox_envs)} envelopes")
    print(f"Sent Items: {len(sent_items_envs)} envelopes")

    # Existing-DB cache
    existing = backend_get("/applications")
    existing_by_company = {a["company"].lower(): a for a in existing}

    def db_lookup(company: str) -> dict | None:
        for c, db_id in memory_db.items():
            if c[0] == company.lower():
                try:
                    with urllib.request.urlopen(f"{BACKEND}/applications/{db_id}") as r:
                        return json.loads(r.read().decode())
                except Exception:
                    memory_db.pop(c, None)
        return existing_by_company.get(company.lower())

    def track(company: str, db_id: int):
        memory_db[(company.lower(), "")] = db_id

    def process(env: dict, source: str):
        env_id = env["id"]
        if env_id in seen:
            return None
        seen.add(env_id)

        sender = env.get("from", "")
        subject = env.get("subject", "")

        # Newsletter raus
        if is_newsletter(sender, subject):
            return None

        # Body holen
        body = fetch_body(env_id)

        # Klassifikation
        status, category = classify(subject, body)

        # Firmen-Extraktion
        firm = None
        position = None

        # 1) Direkte Empfangsbestätigung (Subject + From)
        if "stepstone" in sender.lower() or "stepstone" in subject.lower():
            # Stepstone "ist raus" — Firma muss aus Body geholt werden
            firm = extract_firm_from_body(body) or "Stepstone-Bewerbung"
            _, position = extract_firm_from_subject_stepstone(subject)
        else:
            # Direkt-Mail oder Plattform
            firm = (extract_firm_from_subject_direct(subject, body)
                    or extract_firm_from_from_address(sender)
                    or extract_firm_from_body(body))
            position = position_from_subject(subject)

        # Stepstone ohne Body-Firma: nutze Subject-Position als "Stepstone: <X>"
        if not firm:
            return None
        if not position:
            position = "unbekannt"

        date_str = env.get("date", "")
        applied_at = datetime.utcnow().date().isoformat()
        m_date = re.search(r"(\d{4}-\d{2}-\d{2})", date_str)
        if m_date:
            applied_at = m_date.group(1)

        clean_subject = re.sub(r"^[\s│┆]+", "", subject).strip()
        clean_subject = re.sub(r"\s+", " ", clean_subject)

        event = {
            "env_id": env_id,
            "firm": firm,
            "position": position,
            "status": status,
            "category": category,
            "subject": clean_subject,
            "from": sender,
            "source_mailbox": source,
            "date": date_str,
            "applied_at": applied_at,
            "action": None,
            "app_id": None,
        }

        # DB-Match
        existing_entry = db_lookup(firm)
        if existing_entry:
            existing_id = existing_entry["id"]
            existing_status = existing_entry.get("status")
            if status in ("rejected", "interview", "offer") and existing_status == "open":
                code, _ = backend_put(f"/applications/{existing_id}", {
                    "status": status,
                    "notes": f"Auto-Update {applied_at}: {category} — {clean_subject[:120]}",
                })
                event["action"] = f"UPDATE #{existing_id} -> {code}"
                event["app_id"] = existing_id
                track(firm, existing_id)
            elif status in ("rejected", "interview", "offer"):
                prev = existing_entry.get("notes", "") or ""
                code, _ = backend_put(f"/applications/{existing_id}", {
                    "notes": (prev + f"\n--\nAuto-Update {applied_at}: {category} — {clean_subject[:120]}").strip()
                })
                event["action"] = f"NOTE existing #{existing_id} -> {code}"
                event["app_id"] = existing_id
                track(firm, existing_id)
            else:
                prev = existing_entry.get("notes", "") or ""
                code, _ = backend_put(f"/applications/{existing_id}", {
                    "notes": (prev + f"\n--\nAuto {applied_at}: {category} — {clean_subject[:120]}").strip()
                })
                event["action"] = f"NOTE existing #{existing_id} -> {code}"
                event["app_id"] = existing_id
                track(firm, existing_id)
        else:
            cn = re.search(r"^([A-ZÄÖÜ][a-zäöüß]+ [A-ZÄÖÜ][a-zäöüß]+)", sender)
            code, resp = backend_post("/applications", {
                "company": firm,
                "position": position,
                "status": status,
                "source": source,
                "applied_at": applied_at,
                "notes": f"Auto-Import [{source}]: {clean_subject[:120]}",
                "contact_name": cn.group(1) if cn else None,
            })
            event["action"] = f"CREATE -> {code}"
            if isinstance(resp, dict) and "id" in resp:
                event["app_id"] = resp["id"]
                track(firm, resp["id"])

        return event

    # Verarbeite alle
    for env in inbox_envs:
        ev = process(env, "Inbox")
        if ev:
            new_events.append(ev)
    for env in sent_items_envs:
        ev = process(env, "Sent Items")
        if ev:
            new_events.append(ev)

    # State speichern
    state["seen_env_ids"] = list(seen)[-2000:]
    state["last_run"] = datetime.utcnow().isoformat() + "Z"
    STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")

    print(json.dumps({
        "scanned_inbox": len(inbox_envs),
        "scanned_sent": len(sent_items_envs),
        "new_events": new_events,
    }, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
