import html as _html
import re

from backend.config import GOOGLE_API_SUPPORT, OUTLOOK_SUPPORT, _google_service, _outlook_session
from backend.gmail_helpers_runtime import _gmail_message_headers
from backend.mail_helpers_runtime import (
    _STANDARD_MAIL_FOLDERS,
    _normalize_mail_folder,
    _outlook_build_mail_restriction,
    _outlook_is_primary_mailbox_request,
    _outlook_mailbox_date_field,
    _outlook_mailbox_party,
    _outlook_resolve_folder_by_path,
    _outlook_select_mailboxes,
)
from backend.mail_read_runtime import _GMAIL_FOLDER_QUERIES


def _expand_email_query_keywords(raw_keywords: list[str]) -> list[str]:
    expanded: list[str] = []
    for token in raw_keywords:
        value = str(token or "").strip().lower()
        if not value:
            continue
        if value not in expanded:
            expanded.append(value)
        if value.endswith(("gepland", "planning", "planned")) and "plan" not in expanded:
            expanded.append("plan")
        if value.startswith("in") and len(value) >= 7:
            stem = value[2:]
            if len(stem) >= 4 and stem not in expanded:
                expanded.append(stem)
        if "afspraak" in value and value != "afspraak" and "afspraak" not in expanded:
            expanded.append("afspraak")
    return expanded


def _outlook_search_emails(
    query: str, count: int = 250, mailbox: str = "ALL", folder: str = "Inbox",
    today_only: bool = False, since: str = "", until: str = "",
) -> str:
    if not OUTLOOK_SUPPORT:
        return "Outlook not available: pywin32 not installed."
    try:
        ql = query.lower()
        if not today_only and not since and not until and any(k in ql for k in ["today", "vandaag", "van vandaag", "heute", "aujourd"]):
            today_only = True
        q_core = re.sub(r"[^\w\s]", " ", ql)
        stopwords = {
            "email", "emails", "mail", "mails", "inbox", "outlook",
            "vandaag", "today", "mijn", "my",
            "from", "to", "about", "regarding", "concerning",
        }
        weak_stopwords = {"van", "de", "het", "een", "over", "naar", "voor", "met"}
        words = q_core.split()
        original_words = re.sub(r"[^\w\s]", " ", query).split()
        protected: set[int] = set()
        for idx, word in enumerate(words):
            if word in weak_stopwords:
                for original_index, original_word in enumerate(original_words):
                    if original_word.lower() == word:
                        neighbours = []
                        if original_index > 0:
                            neighbours.append(original_words[original_index - 1])
                        if original_index < len(original_words) - 1:
                            neighbours.append(original_words[original_index + 1])
                        if any(neighbour[0:1].isupper() for neighbour in neighbours if neighbour):
                            protected.add(idx)
                            break
        meaningful = [
            word for index, word in enumerate(words)
            if word not in stopwords and (word not in weak_stopwords or index in protected)
        ]
        if meaningful:
            q_core = " ".join(meaningful)
        else:
            q_core = " ".join(word for word in words if word not in stopwords)
        q_core = q_core.strip()
        match_all = q_core == ""
        base_keywords = q_core.split()
        keywords = _expand_email_query_keywords(base_keywords)
        if match_all and query.strip():
            return f"No emails matching '{query}' — search query too vague after filtering."

        folder = _normalize_mail_folder(folder)
        folder_ids = {"Inbox": 6, "Sent": 5, "Drafts": 16, "Deleted": 3}
        is_custom = folder not in _STANDARD_MAIL_FOLDERS
        folder_id = folder_ids.get(folder, 6)
        date_field = _outlook_mailbox_date_field(folder)

        with _outlook_session() as (_, ns):
            if is_custom:
                custom_folder = _outlook_resolve_folder_by_path(ns, folder)
                if custom_folder:
                    store_name = "Default"
                    try:
                        store = getattr(custom_folder, "Store", None)
                        store_name = str(getattr(store, "DisplayName", "") or "Default")
                    except Exception:
                        pass
                    all_targets = [(store_name, custom_folder)]
                    targets = all_targets
                else:
                    return f"Could not find Outlook folder: {folder}"
            else:
                all_targets = _outlook_select_mailboxes(ns, folder_id=folder_id, mailbox="ALL")
                targets = _outlook_select_mailboxes(ns, folder_id=folder_id, mailbox=mailbox)
            if not targets:
                available = [name for name, _ in _outlook_select_mailboxes(ns, folder_id=folder_id, mailbox="ALL")]
                return (
                    f"No mailbox matched '{mailbox}'. "
                    f"Available mailboxes: {', '.join(available) if available else 'none'}"
                )
            results: list[str] = []
            load_errors: list[str] = []
            total_scanned = 0
            scan_limit = max(count * 25, 2000)
            for store_name, inbox in targets:
                if len(results) >= count:
                    break
                try:
                    items = inbox.Items
                    restriction = _outlook_build_mail_restriction(
                        today_only=today_only, since=since, until=until,
                    ).replace("ReceivedTime", date_field)
                    if restriction:
                        try:
                            items = items.Restrict(restriction)
                        except Exception as restrict_error:
                            load_errors.append(f"{store_name}: Restrict failed ({restrict_error}), scanning all")
                    items.Sort(f"[{date_field}]", True)
                    total = int(items.Count)
                except Exception as folder_error:
                    load_errors.append(f"{store_name}: cannot open folder ({folder_error})")
                    continue
                scanned = 0
                for idx in range(1, total + 1):
                    if len(results) >= count or scanned >= scan_limit:
                        break
                    try:
                        msg = items.Item(idx)
                        msg_class = getattr(msg, "MessageClass", "IPM.Note")
                        if not str(msg_class).startswith("IPM.Note"):
                            continue
                        scanned += 1
                        total_scanned += 1
                        subject = (msg.Subject or "").lower()
                        full_body = msg.Body or ""
                        body_lower = full_body.lower()
                        sender_lower = ((getattr(msg, "SenderName", "") or "") + " " + (getattr(msg, "SenderEmailAddress", "") or "")).lower()
                        hit = match_all or (all(keyword in subject or keyword in body_lower or keyword in sender_lower for keyword in keywords) if keywords else True)
                        if hit:
                            party_label, party_value = _outlook_mailbox_party(msg, folder)
                            message_dt = getattr(msg, date_field, "")
                            results.append(
                                f"Mailbox: {store_name}\n"
                                f"Folder: {folder}\n"
                                f"Message ID: {getattr(msg, 'EntryID', '') or '?'}\n"
                                f"Thread ID: {getattr(msg, 'ConversationID', '') or '?'}\n"
                                f"{party_label}: {party_value}\n"
                                f"Subject: {msg.Subject}\n"
                                f"Date: {message_dt}\n"
                                f"Preview: {full_body.strip()[:300]}"
                            )
                    except Exception:
                        pass
            if not results and _outlook_is_primary_mailbox_request(mailbox) and len(all_targets) > 1:
                load_errors.append("Primary mailbox returned no emails; retried across all Outlook mailboxes")
                scan_limit = max(count * 25, 2000)
                for store_name, inbox in all_targets:
                    if len(results) >= count:
                        break
                    try:
                        items = inbox.Items
                        restriction = _outlook_build_mail_restriction(
                            today_only=today_only, since=since, until=until,
                        ).replace("ReceivedTime", date_field)
                        if restriction:
                            try:
                                items = items.Restrict(restriction)
                            except Exception as restrict_error:
                                load_errors.append(f"{store_name}: Restrict failed ({restrict_error}), scanning all")
                        items.Sort(f"[{date_field}]", True)
                        total = int(items.Count)
                    except Exception as folder_error:
                        load_errors.append(f"{store_name}: cannot open folder ({folder_error})")
                        continue
                    scanned = 0
                    for idx in range(1, total + 1):
                        if len(results) >= count or scanned >= scan_limit:
                            break
                        try:
                            msg = items.Item(idx)
                            msg_class = getattr(msg, "MessageClass", "IPM.Note")
                            if not str(msg_class).startswith("IPM.Note"):
                                continue
                            scanned += 1
                            total_scanned += 1
                            subject = (msg.Subject or "").lower()
                            full_body = msg.Body or ""
                            body_lower = full_body.lower()
                            sender_lower = ((getattr(msg, "SenderName", "") or "") + " " + (getattr(msg, "SenderEmailAddress", "") or "")).lower()
                            hit = match_all or (all(keyword in subject or keyword in body_lower or keyword in sender_lower for keyword in keywords) if keywords else True)
                            if hit:
                                party_label, party_value = _outlook_mailbox_party(msg, folder)
                                message_dt = getattr(msg, date_field, "")
                                results.append(
                                    f"Mailbox: {store_name}\n"
                                    f"Folder: {folder}\n"
                                    f"Message ID: {getattr(msg, 'EntryID', '') or '?'}\n"
                                    f"Thread ID: {getattr(msg, 'ConversationID', '') or '?'}\n"
                                    f"{party_label}: {party_value}\n"
                                    f"Subject: {msg.Subject}\n"
                                    f"Date: {message_dt}\n"
                                    f"Preview: {full_body.strip()[:300]}"
                                )
                        except Exception:
                            pass
            if not results and len(keywords) >= 2:
                ranked: list[tuple[int, str]] = []
                seen_ids: set[str] = set()
                scan_limit = max(count * 40, 4000)
                for store_name, inbox in all_targets:
                    try:
                        items = inbox.Items
                        restriction = _outlook_build_mail_restriction(
                            today_only=today_only, since=since, until=until,
                        ).replace("ReceivedTime", date_field)
                        if restriction:
                            try:
                                items = items.Restrict(restriction)
                            except Exception:
                                pass
                        items.Sort(f"[{date_field}]", True)
                        total = int(items.Count)
                    except Exception:
                        continue
                    scanned = 0
                    for idx in range(1, total + 1):
                        if scanned >= scan_limit:
                            break
                        try:
                            msg = items.Item(idx)
                            msg_class = getattr(msg, "MessageClass", "IPM.Note")
                            if not str(msg_class).startswith("IPM.Note"):
                                continue
                            scanned += 1
                            total_scanned += 1
                            entry_id = str(getattr(msg, "EntryID", "") or "").strip()
                            if entry_id and entry_id in seen_ids:
                                continue
                            subject = (msg.Subject or "").lower()
                            full_body = msg.Body or ""
                            body_lower = full_body.lower()
                            sender_lower = ((getattr(msg, "SenderName", "") or "") + " " + (getattr(msg, "SenderEmailAddress", "") or "")).lower()
                            score = sum(1 for keyword in keywords if keyword in subject or keyword in body_lower or keyword in sender_lower)
                            if score <= 0:
                                continue
                            if entry_id:
                                seen_ids.add(entry_id)
                            party_label, party_value = _outlook_mailbox_party(msg, folder)
                            message_dt = getattr(msg, date_field, "")
                            ranked.append((
                                score,
                                f"Mailbox: {store_name}\n"
                                f"Folder: {folder}\n"
                                f"Message ID: {entry_id or '?'}\n"
                                f"Thread ID: {getattr(msg, 'ConversationID', '') or '?'}\n"
                                f"{party_label}: {party_value}\n"
                                f"Subject: {msg.Subject}\n"
                                f"Date: {message_dt}\n"
                                f"Preview: {full_body.strip()[:300]}\n"
                                f"Match score: {score}/{len(keywords)}"
                            ))
                        except Exception:
                            pass
                if ranked:
                    ranked.sort(key=lambda item: item[0], reverse=True)
                    results = [item[1] for item in ranked[:count]]

        diagnostic_line = (
            f"[Search diagnostics: scanned {total_scanned} items, returned {len(results)} result(s), "
            f"scan_limit {scan_limit} per mailbox pass]"
        )
        parts = [diagnostic_line]
        if results:
            parts.append("\n\n---\n\n".join(results))
        else:
            parts.append(f"No emails matching '{query}'.")
        if load_errors:
            parts.append("\n[Warnings: " + "; ".join(load_errors) + "]")
        return "\n\n".join(parts)
    except Exception as e:
        return f"Outlook search error: {e}"


def _gmail_search_emails(query: str, count: int = 20, folder: str = "Inbox") -> str:
    if not GOOGLE_API_SUPPORT:
        return "Google API libraries not installed."
    service = _google_service("gmail", "v1")
    if not service:
        return "Google not authorized. Ask the user to click 'Authorize Google' in Settings → Integrations."
    try:
        folder = _normalize_mail_folder(folder)
        q_parts: list[str] = []
        folder_query = _GMAIL_FOLDER_QUERIES.get(folder)
        if folder_query and folder != "Inbox":
            q_parts.append(folder_query)
        elif folder not in _STANDARD_MAIL_FOLDERS:
            q_parts.append(f"label:{folder.replace(' ', '-')}")
        q_parts.append(query)
        effective_query = " ".join(part for part in q_parts if part.strip())
        diagnostics_mode = "primary"
        diagnostics_relaxed_keywords: list[str] = []
        resp = service.users().messages().list(
            userId="me", q=effective_query, maxResults=min(count, 100)
        ).execute()
        messages = resp.get("messages", [])
        if not messages:
            keyword_candidates = re.findall(r"[A-Za-z0-9@._+-]+", str(query or "").lower())
            keyword_stopwords = {
                "mail", "email", "e-mail", "inbox", "mijn", "my", "de", "het", "een",
                "zoek", "search", "find", "lookup", "in", "op", "via", "voor", "van", "from",
            }
            keywords = [
                keyword for keyword in keyword_candidates
                if (len(keyword) >= 3 or keyword.isupper() or any(char.isdigit() for char in keyword))
                and keyword not in keyword_stopwords
            ]
            keywords = _expand_email_query_keywords(keywords)
            if len(keywords) >= 2:
                diagnostics_mode = "relaxed-keyword-union"
                diagnostics_relaxed_keywords = keywords[:6]
                collected: dict[str, dict] = {}
                for keyword in keywords[:6]:
                    kw_parts = list(q_parts)
                    kw_parts[-1] = keyword
                    kw_query = " ".join(part for part in kw_parts if part.strip())
                    kw_resp = service.users().messages().list(
                        userId="me", q=kw_query, maxResults=min(count, 100)
                    ).execute()
                    for stub in kw_resp.get("messages", []) or []:
                        message_id = str(stub.get("id") or "").strip()
                        if message_id and message_id not in collected:
                            collected[message_id] = stub
                messages = list(collected.values())[: min(count, 100)]
            if not messages:
                return f"No emails matching '{query}'."
        results = []
        for msg_stub in messages:
            msg = service.users().messages().get(
                userId="me", id=msg_stub["id"], format="metadata",
                metadataHeaders=["From", "To", "Subject", "Date"],
            ).execute()
            headers = _gmail_message_headers(msg)
            snippet = _html.unescape(msg.get("snippet", ""))
            party_header = "To" if folder == "Sent" else "From"
            results.append(
                f"Mailbox: Gmail\n"
                f"Folder: {folder}\n"
                f"Message ID: {msg.get('id', '?')}\n"
                f"Thread ID: {msg.get('threadId', '?')}\n"
                f"{party_header}: {headers.get(party_header, '?')}\n"
                f"Subject: {headers.get('Subject', '(no subject)')}\n"
                f"Date: {headers.get('Date', '?')}\n"
                f"Preview: {snippet[:300]}"
            )
        diag = (
            f"[Search diagnostics: mode={diagnostics_mode}; query='{effective_query}'; "
            f"returned={len(messages)}; requested_count={min(count, 100)}"
        )
        if diagnostics_relaxed_keywords:
            diag += f"; relaxed_keywords={', '.join(diagnostics_relaxed_keywords)}"
        diag += "]"
        return diag + "\n\n" + "\n\n---\n\n".join(results)
    except Exception as e:
        return f"Gmail search error: {e}"