"""
AI_Studioxyz Outlook Email Finder.
Searches Outlook emails using natural language or keyword matching.
"""
import sys
import json
import time
import argparse
from datetime import datetime, timedelta
from typing import Optional

try:
    from colorama import init, Fore, Style
    init(autoreset=True)
    HAS_COLOR = True
except ImportError:
    HAS_COLOR = False

    class _Noop:
        def __getattr__(self, _: str) -> str:
            return ""

    Fore = _Noop()
    Style = _Noop()


# ── Outlook helpers ──────────────────────────────────────────────────────────

def connect_outlook():
    """Connect to running Outlook instance via COM."""
    try:
        import win32com.client  # type: ignore
        outlook = win32com.client.Dispatch("Outlook.Application")
        return outlook.GetNamespace("MAPI")
    except ImportError:
        print(Fore.RED + "\nFehler: pywin32 ist nicht installiert.")
        print("Bitte führe zuerst setup.bat aus.\n")
        sys.exit(1)
    except Exception:
        print(Fore.RED + "\nFehler: Outlook konnte nicht geöffnet werden.")
        print("Bitte stelle sicher, dass Outlook auf deinem Computer installiert")
        print("und geöffnet ist, und versuche es erneut.\n")
        sys.exit(1)


def get_sent_folders(namespace, all_folders: bool) -> list:
    """Return list of Outlook folder objects to search."""
    folders = []
    if not all_folders:
        # olFolderSentMail = 5
        folders.append(namespace.GetDefaultFolder(5))
    else:
        # Walk all stores (accounts) and collect every folder recursively
        for store in namespace.Stores:
            root = store.GetRootFolder()
            folders.extend(_collect_folders(root))
    return folders


def _collect_folders(folder) -> list:
    """Recursively collect all subfolders."""
    result = [folder]
    for sub in folder.Folders:
        result.extend(_collect_folders(sub))
    return result


def list_search_folders(namespace) -> list:
    """Return all Outlook folders available for manual selection."""
    folders = []
    for store in namespace.Stores:
        try:
            root = store.GetRootFolder()
            folders.extend(_collect_folders(root))
        except Exception:
            continue
    return folders


def folder_display_name(folder) -> str:
    """Return a readable Outlook folder label."""
    for attr in ("FolderPath", "Name"):
        try:
            value = getattr(folder, attr)
            if value:
                return str(value).replace("\\\\", " / ").strip()
        except Exception:
            continue
    return "Outlook-Ordner"


def load_emails_from_folders(folders: list, days: int) -> list[dict]:
    """Load email metadata from the provided Outlook folders."""
    cutoff = datetime.now() - timedelta(days=days) if days > 0 else None

    emails: list[dict] = []
    for folder in folders:
        try:
            items = folder.Items
        except Exception:
            continue
        sorted_newest_first = False
        try:
            items.Sort("[SentOn]", True)  # newest first
            sorted_newest_first = True
        except Exception:
            pass  # some folders don't support sorting
        for item in items:
            try:
                # Only MailItem objects are searchable here.
                if item.Class != 43:  # 43 = olMail
                    continue
                sent_on = getattr(item, "SentOn", None)
                received_on = getattr(item, "ReceivedTime", None)
                raw_dt = sent_on or received_on
                if raw_dt is None:
                    continue
                mail_dt = datetime(
                    raw_dt.year, raw_dt.month, raw_dt.day,
                    raw_dt.hour, raw_dt.minute, raw_dt.second,
                )
                if cutoff and mail_dt < cutoff and sorted_newest_first:
                    break  # items are sorted newest-first; stop early
                if cutoff and mail_dt < cutoff:
                    continue
                body_preview = (item.Body or "")[:100].replace("\r\n", " ").replace("\n", " ")
                to_value = getattr(item, "To", "") or ""
                sender_value = getattr(item, "SenderName", "") or getattr(item, "SenderEmailAddress", "") or ""
                counterparty = to_value or sender_value
                direction = "Gesendet" if to_value else "Empfangen"
                emails.append({
                    "index": len(emails),
                    "date": mail_dt,
                    "date_str": mail_dt.strftime("%d. %B %Y"),
                    "to": to_value,
                    "sender": sender_value,
                    "counterparty": counterparty,
                    "direction": direction,
                    "subject": item.Subject or "",
                    "preview": body_preview,
                    "_com_item": item,
                })
            except Exception:
                continue  # skip corrupted / non-mail items

    return emails


def load_emails(namespace, all_folders: bool, days: int) -> list[dict]:
    """Load email metadata from Outlook folder(s)."""
    folders = get_sent_folders(namespace, all_folders)
    return load_emails_from_folders(folders, days)


# ── Search modes ─────────────────────────────────────────────────────────────

def search_with_claude(emails: list[dict], query: str) -> list[int]:
    """Use Claude claude-haiku-4-5 to rank emails by relevance. Returns list of indices."""
    try:
        import anthropic  # type: ignore
        from dotenv import load_dotenv  # type: ignore
        import os

        load_dotenv()
        api_key = os.getenv("ANTHROPIC_API_KEY", "")
        if not api_key:
            return []

        client = anthropic.Anthropic(api_key=api_key)

        # Build compact metadata for the prompt (no full body — saves tokens)
        email_metadata = [
            {
                "i": e["index"],
                "date": e["date_str"],
                "to": e["to"],
                "subject": e["subject"],
                "preview": e["preview"],
            }
            for e in emails
        ]

        prompt = (
            f'Du bist ein E-Mail-Assistent. Hier sind {len(emails)} gesendete E-Mails als JSON.\n'
            f'Finde die 5 relevantesten für diese Suchanfrage: "{query}"\n'
            'Antworte NUR mit JSON: {"results": [0, 3, 7, ...]} '
            '(indices der relevantesten Mails, absteigend nach Relevanz sortiert)\n\n'
            f'E-Mails:\n{json.dumps(email_metadata, ensure_ascii=False)}'
        )

        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=256,
            messages=[{"role": "user", "content": prompt}],
        )

        raw = response.content[0].text.strip()
        # Extract JSON even if Claude wraps it in markdown fences
        if "```" in raw:
            raw = raw.split("```")[1].lstrip("json").strip()
        data = json.loads(raw)
        return [int(i) for i in data.get("results", [])][:5]

    except Exception:
        return []  # fall back to keyword search


def search_keyword(emails: list[dict], query: str) -> list[int]:
    """Simple keyword match across subject, recipient and preview."""
    terms = query.lower().split()
    scored: list[tuple[int, int]] = []
    for email in emails:
        haystack = " ".join([
            email["subject"].lower(),
            email.get("counterparty", "").lower(),
            email.get("to", "").lower(),
            email.get("sender", "").lower(),
            email["preview"].lower(),
        ])
        score = sum(1 for term in terms if term in haystack)
        if score > 0:
            scored.append((email["index"], score))
    # Sort by score descending, take top 5
    scored.sort(key=lambda x: x[1], reverse=True)
    return [idx for idx, _ in scored[:5]]


def search_emails(emails: list[dict], query: str, use_ai: bool) -> list[dict]:
    """Run the appropriate search and return matched email dicts."""
    if use_ai:
        indices = search_with_claude(emails, query)
        if not indices:
            # Claude failed or no key — fall back silently
            use_ai = False

    if not use_ai:
        indices = search_keyword(emails, query)

    # Map indices back to email dicts; guard against out-of-range
    index_map = {e["index"]: e for e in emails}
    return [index_map[i] for i in indices if i in index_map]


# ── Output ───────────────────────────────────────────────────────────────────

GERMAN_MONTHS = {
    1: "Januar", 2: "Februar", 3: "März", 4: "April",
    5: "Mai", 6: "Juni", 7: "Juli", 8: "August",
    9: "September", 10: "Oktober", 11: "November", 12: "Dezember",
}


def format_date(dt: datetime) -> str:
    return f"{dt.day}. {GERMAN_MONTHS[dt.month]} {dt.year}"


def format_results_text(results: list[dict]) -> str:
    """Format results as plain text for clipboard export."""
    lines = []
    for i, email in enumerate(results, start=1):
        date_str = format_date(email["date"])
        lines.append(f"[{i}] {date_str} — An: {email['to']}")
        lines.append(f"    Betreff: {email['subject']}")
        lines.append(f'    Vorschau: "...{email["preview"][:150]}..."')
        lines.append("")
    return "\n".join(lines).rstrip()


def print_results(results: list[dict], elapsed: float, total: int) -> None:
    """Pretty-print search results."""
    count = len(results)
    divider = "─" * 46

    if count == 0:
        print(Fore.YELLOW + "\nKeine Treffer gefunden.\n")
        print(f"Suche dauerte {elapsed:.1f}s | {total} gesendete Mails durchsucht\n")
        return

    print(Fore.GREEN + f"\n✓ {count} Treffer gefunden")
    print(divider)

    for i, email in enumerate(results, start=1):
        date_str = format_date(email["date"])
        print(
            Fore.YELLOW + f"[{i}] {date_str} "
            + Style.RESET_ALL + "— An: "
            + Fore.YELLOW + email["to"]
        )
        print(
            Style.RESET_ALL + "    Betreff: "
            + Style.BRIGHT + email["subject"]
        )
        preview = email["preview"][:150]
        print(Style.RESET_ALL + f'    Vorschau: "...{preview}..."')
        if i < count:
            print()

    print(divider)
    print(Style.DIM + f"Suche dauerte {elapsed:.1f}s | {total} gesendete Mails durchsucht\n")


# ── Open email in Outlook ───────────────────────────────────────────────────

def open_email_prompt(results: list[dict]) -> None:
    """Let user pick a result number to open in Outlook, or press Enter to skip."""
    if not results:
        return
    print(Style.DIM + "Nummer eingeben zum Öffnen in Outlook (Enter = überspringen):")
    try:
        choice = input("Öffnen> ").strip()
    except (KeyboardInterrupt, EOFError):
        return
    if not choice:
        return
    try:
        idx = int(choice)
        if 1 <= idx <= len(results):
            item = results[idx - 1].get("_com_item")
            if item:
                item.Display()
                print(Fore.GREEN + "E-Mail wird in Outlook geöffnet...")
            else:
                print(Fore.RED + "COM-Objekt nicht verfügbar.")
        else:
            print(Fore.RED + f"Bitte eine Zahl zwischen 1 und {len(results)} eingeben.")
    except ValueError:
        print(Fore.RED + "Ungültige Eingabe.")


# ── Clipboard ───────────────────────────────────────────────────────────────

def copy_to_clipboard(results: list[dict]) -> None:
    """Copy results to clipboard via pyperclip."""
    try:
        import pyperclip  # type: ignore
    except ImportError:
        print(Fore.RED + "pyperclip ist nicht installiert. Bitte: pip install pyperclip")
        return
    text = format_results_text(results)
    pyperclip.copy(text)
    print(Fore.GREEN + "✓ Ergebnisse in die Zwischenablage kopiert.")


# ── Entry point ──────────────────────────────────────────────────────────────

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="AI_Studioxyz Outlook Email Finder",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Beispiele:\n"
            '  python find_email.py "Angebot Huber Bau"\n'
            '  python find_email.py "Rechnung März" --days 90\n'
            '  python find_email.py "Support Ticket" --all-folders\n'
            '  python find_email.py --copy "Rechnung"\n'
        ),
    )
    parser.add_argument(
        "query",
        nargs="?",
        help="Suchanfrage (optional — startet interaktiven Modus wenn weggelassen)",
    )
    parser.add_argument(
        "--days",
        type=int,
        default=730,
        metavar="N",
        help="Zeitraum in Tagen (Standard: 730 = 2 Jahre)",
    )
    parser.add_argument(
        "--all-folders",
        action="store_true",
        help="Alle Ordner durchsuchen, nicht nur Gesendet",
    )
    parser.add_argument(
        "--copy",
        action="store_true",
        help="Ergebnisse in die Zwischenablage kopieren",
    )
    return parser


def check_ai_available() -> bool:
    """Return True if ANTHROPIC_API_KEY is set."""
    try:
        from dotenv import load_dotenv  # type: ignore
        import os
        load_dotenv()
        return bool(os.getenv("ANTHROPIC_API_KEY", ""))
    except ImportError:
        return False


def run_search(query: str, namespace, emails: list[dict], use_ai: bool, copy: bool) -> list[dict]:
    """Execute search, print results, optionally copy & offer to open."""
    t0 = time.perf_counter()
    results = search_emails(emails, query, use_ai)
    elapsed = time.perf_counter() - t0

    print_results(results, elapsed, len(emails))

    if copy and results:
        copy_to_clipboard(results)

    open_email_prompt(results)
    return results


def show_first_run_hint() -> None:
    """Show a short usage hint on first run (creates a marker file)."""
    import os
    marker = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".first_run_done")
    if os.path.exists(marker):
        return
    print(Fore.CYAN + "\n╔══════════════════════════════════════════════╗")
    print(Fore.CYAN + "║  Paperclip Outlook Finder — Erste Schritte   ║")
    print(Fore.CYAN + "╚══════════════════════════════════════════════╝")
    print()
    print("  Suche starten:")
    print(Fore.YELLOW + '    python find_email.py "Angebot Huber"')
    print()
    print("  Interaktiver Modus:")
    print(Fore.YELLOW + "    python find_email.py")
    print()
    print("  Weitere Optionen: --days N, --all-folders, --copy")
    print("  Details: README.md\n")
    try:
        with open(marker, "w") as f:
            f.write("ok")
    except OSError:
        pass


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()

    show_first_run_hint()

    # Connect and load emails
    print(Style.DIM + "\nVerbinde mit Outlook...")
    ns = connect_outlook()

    print(Style.DIM + f"Lade E-Mails der letzten {args.days} Tage...")
    emails = load_emails(ns, args.all_folders, args.days)

    if not emails:
        print(Fore.YELLOW + "\nKeine gesendeten E-Mails im angegebenen Zeitraum gefunden.\n")
        sys.exit(0)

    use_ai = check_ai_available()
    if use_ai:
        print(Style.DIM + "KI-Suche aktiv (Claude claude-haiku-4-5)...")
    else:
        print(Style.DIM + "Stichwortsuche aktiv (kein API-Key gefunden)...")

    if args.query:
        run_search(args.query, ns, emails, use_ai, args.copy)
    else:
        # Interactive mode: loop until "quit"
        print(Fore.CYAN + "\nInteraktiver Modus — tippe 'quit' zum Beenden\n")
        while True:
            try:
                query = input("Suche> ").strip()
            except (KeyboardInterrupt, EOFError):
                print("\nBeendet.")
                break
            if not query:
                continue
            if query.lower() == "quit":
                print("Beendet.")
                break
            run_search(query, ns, emails, use_ai, args.copy)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nBeendet.")
    except Exception as exc:
        print(Fore.RED + f"\nUnerwarteter Fehler: {exc}")
        print("Bitte stelle sicher, dass Outlook geöffnet ist und versuche es erneut.")
        print("Falls das Problem bestehen bleibt, führe setup.bat erneut aus.\n")
        sys.exit(1)
