import base64
import json
import re
from datetime import datetime, timedelta
from pathlib import Path

from backend.config import WORKSPACE_ROOT
from backend.content_extraction import extract_source


def _outlook_folder_store_name(folder, fallback: str = "Default") -> str:
    try:
        store = getattr(folder, "Store", None)
        name = str(getattr(store, "DisplayName", "") or "").strip()
        if name:
            return name
    except Exception:
        pass
    return fallback


def _outlook_select_mailboxes(ns, folder_id: int = 6, mailbox: str = "ALL") -> list[tuple[str, object]]:
    targets: list[tuple[str, object]] = []
    stores = getattr(ns, "Stores", None)
    if stores is not None:
        for i in range(1, int(stores.Count) + 1):
            try:
                store = stores.Item(i)
                display = str(getattr(store, "DisplayName", "") or f"Store {i}")
                folder = store.GetDefaultFolder(folder_id)
                if folder is not None:
                    targets.append((display, folder))
            except Exception:
                pass
    if not targets:
        try:
            targets.append(("Default", ns.GetDefaultFolder(folder_id)))
        except Exception:
            pass
    requested = (mailbox or "ALL").strip()
    if not requested or requested.upper() == "ALL":
        return targets
    if requested.upper() in {"DEFAULT", "PRIMARY"}:
        try:
            default_folder = ns.GetDefaultFolder(folder_id)
            return [(_outlook_folder_store_name(default_folder), default_folder)]
        except Exception:
            return []
    requested_l = requested.lower()
    exact_matches = [(name, folder) for name, folder in targets if name.lower() == requested_l]
    if exact_matches:
        return exact_matches
    startswith_matches = [(name, folder) for name, folder in targets if name.lower().startswith(requested_l)]
    if len(startswith_matches) == 1:
        return startswith_matches
    contains_matches = [(name, folder) for name, folder in targets if requested_l in name.lower()]
    if len(contains_matches) == 1:
        return contains_matches
    return []


def _outlook_parse_datetime(value: str) -> datetime | None:
    if not value:
        return None
    value = value.strip()
    if not value:
        return None
    formats = [
        "%Y-%m-%d", "%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M:%S",
        "%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S",
        "%d-%m-%Y", "%d-%m-%Y %H:%M",
        "%m/%d/%Y", "%m/%d/%Y %H:%M",
    ]
    for fmt in formats:
        try:
            return datetime.strptime(value, fmt)
        except Exception:
            pass
    try:
        return datetime.fromisoformat(value)
    except Exception:
        return None


def _is_date_only(value: str) -> bool:
    if not value:
        return False
    v = value.strip()
    return bool(re.fullmatch(r"\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}|\d{1,2}/\d{1,2}/\d{4}", v))


def _outlook_format_dt(dt: datetime) -> str:
    import ctypes

    class SYSTEMTIME(ctypes.Structure):
        _fields_ = [
            ("wYear", ctypes.c_ushort), ("wMonth", ctypes.c_ushort),
            ("wDayOfWeek", ctypes.c_ushort), ("wDay", ctypes.c_ushort),
            ("wHour", ctypes.c_ushort), ("wMinute", ctypes.c_ushort),
            ("wSecond", ctypes.c_ushort), ("wMilliseconds", ctypes.c_ushort),
        ]

    st = SYSTEMTIME(wYear=dt.year, wMonth=dt.month, wDay=dt.day)
    buf = ctypes.create_unicode_buffer(80)
    ctypes.windll.kernel32.GetDateFormatW(0x0400, 1, ctypes.byref(st), None, buf, 80)
    return f"{buf.value} {dt.hour:02d}:{dt.minute:02d}"


def _outlook_build_mail_restriction(today_only: bool = False, since: str = "", until: str = "") -> str:
    now = datetime.now()
    since_dt = _outlook_parse_datetime(since)
    until_dt = _outlook_parse_datetime(until)
    if since_dt and _is_date_only(since):
        since_dt = datetime(since_dt.year, since_dt.month, since_dt.day, 0, 0, 0)
    if until_dt and _is_date_only(until):
        until_dt = datetime(until_dt.year, until_dt.month, until_dt.day, 23, 59, 59)
    if today_only and not since_dt and not until_dt:
        since_dt = datetime(now.year, now.month, now.day, 0, 0, 0)
        until_dt = datetime(now.year, now.month, now.day, 23, 59, 59)
    terms: list[str] = []
    if since_dt:
        terms.append(f"[ReceivedTime] >= '{_outlook_format_dt(since_dt)}'")
    if until_dt:
        terms.append(f"[ReceivedTime] <= '{_outlook_format_dt(until_dt)}'")
    return " AND ".join(terms)


def _outlook_build_calendar_restriction(days_ahead: int = 7, since: str = "", until: str = "") -> tuple[datetime, datetime, str]:
    now = datetime.now()
    since_dt = _outlook_parse_datetime(since)
    until_dt = _outlook_parse_datetime(until)
    if since_dt and _is_date_only(since):
        since_dt = datetime(since_dt.year, since_dt.month, since_dt.day, 0, 0, 0)
    if until_dt and _is_date_only(until):
        until_dt = datetime(until_dt.year, until_dt.month, until_dt.day, 23, 59, 59)
    if not since_dt:
        since_dt = now
    if not until_dt:
        until_dt = since_dt + timedelta(days=days_ahead)
    restriction = (
        f"[Start] >= '{_outlook_format_dt(since_dt)}' AND "
        f"[Start] <= '{_outlook_format_dt(until_dt)}'"
    )
    return since_dt, until_dt, restriction


_MAIL_FOLDER_ALIASES = {
    "inbox": "Inbox",
    "postvak in": "Inbox",
    "sent": "Sent",
    "sent items": "Sent",
    "verzonden": "Sent",
    "verzonden items": "Sent",
    "drafts": "Drafts",
    "concepten": "Drafts",
    "deleted": "Deleted",
    "deleted items": "Deleted",
    "trash": "Deleted",
    "prullenbak": "Deleted",
}


def _normalize_mail_folder(folder: str, default: str = "Inbox") -> str:
    raw = str(folder or "").strip()
    if not raw:
        return default
    alias = _MAIL_FOLDER_ALIASES.get(raw.lower())
    if alias:
        return alias
    return raw


def _normalize_outlook_party_value(value: str) -> str:
    raw = str(value or "").strip()
    if not raw:
        return ""
    if raw.startswith("/O=") or "/CN=" in raw.upper():
        leaf = raw.split("/")[-1].strip()
        if leaf.upper().startswith("CN="):
            leaf = leaf[3:].strip()
        if leaf:
            return leaf
    return raw


def _outlook_mailbox_party(msg, folder: str) -> tuple[str, str]:
    folder_name = _normalize_mail_folder(folder)
    if folder_name in {"Sent", "Drafts"}:
        return "To", str(getattr(msg, "To", "") or "")

    sender = ""
    try:
        sender = msg.SenderName
    except Exception:
        try:
            sender = msg.SenderEmailAddress
        except Exception:
            pass
    if not str(sender or "").strip():
        try:
            sender = msg.SenderEmailAddress
        except Exception:
            pass
    return "From", _normalize_outlook_party_value(str(sender or ""))


def _outlook_mailbox_date_field(folder: str) -> str:
    return "SentOn" if _normalize_mail_folder(folder) == "Sent" else "ReceivedTime"


_STANDARD_MAIL_FOLDERS = {"Inbox", "Sent", "Drafts", "Deleted"}


def _outlook_resolve_folder_by_path(ns, folder_path: str):
    parts = [p for p in folder_path.replace("\\", "/").split("/") if p.strip()]
    if not parts:
        return None
    stores = getattr(ns, "Stores", None)
    if stores is None:
        return None
    for i in range(1, int(stores.Count) + 1):
        try:
            store = stores.Item(i)
            display = str(getattr(store, "DisplayName", "") or "").strip()
            root = store.GetRootFolder()
            search_parts = parts
            if display and parts[0].lower() == display.lower():
                search_parts = parts[1:]
            folder = root
            for part in search_parts:
                found = False
                subfolders = folder.Folders
                for j in range(1, int(subfolders.Count) + 1):
                    sub = subfolders.Item(j)
                    if str(getattr(sub, "Name", "")).lower() == part.lower():
                        folder = sub
                        found = True
                        break
                if not found:
                    folder = None
                    break
            if folder and folder != root:
                return folder
        except Exception:
            continue
    return None


def _outlook_is_primary_mailbox_request(mailbox: str) -> bool:
    requested = str(mailbox or "").strip().upper()
    return requested in {"DEFAULT", "PRIMARY"}


_ALLOWED_ATTACHMENT_DIRS = ("output", "attachments", "downloads")


def _resolve_attachment_path(raw: str) -> Path | None:
    if not raw:
        return None
    p = Path(raw)
    if not p.is_absolute():
        p = WORKSPACE_ROOT / p
    try:
        p = p.resolve()
    except Exception:
        return None
    if not p.is_file():
        return None
    for allowed in _ALLOWED_ATTACHMENT_DIRS:
        allowed_dir = (WORKSPACE_ROOT / allowed).resolve()
        try:
            p.relative_to(allowed_dir)
            return p
        except ValueError:
            continue
    return None


def _normalize_email_body_text(value: str, *, strip: bool = False) -> str:
    text = str(value or "").replace("\r\n", "\n").replace("\r", "\n")
    return text.strip() if strip else text


def _normalize_email_header_value(value: str) -> str:
    return re.sub(r"[\r\n]+", " ", str(value or "")).strip()


def _gmail_message_raw_bytes(mime_msg) -> str:
    import email.policy

    return base64.urlsafe_b64encode(
        mime_msg.as_bytes(policy=email.policy.SMTP)
    ).decode("utf-8")


def _outlook_get_mail_item(ns, message_id: str):
    mid = str(message_id or "").strip()
    if not mid:
        return None
    try:
        item = ns.GetItemFromID(mid)
        if item is not None:
            return item
    except Exception:
        pass
    try:
        for i in range(1, int(ns.Stores.Count) + 1):
            try:
                store = ns.Stores.Item(i)
                item = ns.GetItemFromID(mid, store.StoreID)
                if item is not None:
                    return item
            except Exception:
                continue
    except Exception:
        pass
    return None


_EMAIL_ATTACHMENT_ANALYZER_EXTENSIONS = {
    ".pdf",
    ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
    ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp",
    ".txt", ".csv", ".json", ".xml", ".html", ".htm", ".md", ".log",
}
_EMAIL_ATTACHMENT_INLINE_IMAGE_EXTENSIONS = {
    ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp",
}


def _limit_attachment_excerpt(value: str, max_chars: int = 6000) -> str:
    text = str(value or "").strip()
    if len(text) <= max_chars:
        return text
    return text[: max_chars - 1].rstrip() + "…"


def _safe_attachment_filename(filename: str, fallback: str = "attachment.bin") -> str:
    cleaned = re.sub(r'[<>:"/\\|?*]+', "_", str(filename or "").strip())
    cleaned = cleaned.strip().strip(".")
    return cleaned or fallback


def _attachment_is_probably_inline(
    filename: str,
    mime_type: str = "",
    size_bytes: int = 0,
    disposition: str = "",
) -> bool:
    lower_name = str(filename or "").strip().lower()
    lower_mime = str(mime_type or "").strip().lower()
    lower_disposition = str(disposition or "").strip().lower()
    if "inline" in lower_disposition and "attachment" not in lower_disposition:
        return True
    if not lower_name and size_bytes <= 65536:
        return True
    if size_bytes < 1024 and (
        lower_mime.startswith("image/")
        or any(lower_name.endswith(ext) for ext in _EMAIL_ATTACHMENT_INLINE_IMAGE_EXTENSIONS)
    ):
        return True
    return False


def _attachment_analysis_to_text(raw_result: str, *, fallback_name: str, max_chars: int = 6000) -> str:
    try:
        payload = json.loads(str(raw_result or ""))
    except Exception:
        return _limit_attachment_excerpt(raw_result, max_chars=max_chars)

    if str(payload.get("status") or "").lower() == "error":
        message = str(payload.get("message") or "Attachment analysis failed.").strip()
        return _limit_attachment_excerpt(f"[ATTACHMENT: {fallback_name}]\n{message}", max_chars=max_chars)

    data = payload.get("data") or {}
    if not isinstance(data, dict):
        return _limit_attachment_excerpt(
            f"[ATTACHMENT: {fallback_name}]\n{json.dumps(data, ensure_ascii=False, default=str)}",
            max_chars=max_chars,
        )

    analysis = data.get("analysis") if isinstance(data.get("analysis"), dict) else {}
    lines = [f"[ATTACHMENT: {str(data.get('file_name') or fallback_name).strip() or fallback_name}]"]
    file_type = str(data.get("file_type") or Path(fallback_name).suffix or "").strip()
    file_size = data.get("file_size")
    if file_type:
        lines.append(f"Type: {file_type}")
    if file_size:
        lines.append(f"Size: {file_size} bytes")

    sheet_names = analysis.get("sheet_names") if isinstance(analysis, dict) else None
    if isinstance(sheet_names, list) and sheet_names:
        lines.append("Sheets: " + ", ".join(str(item) for item in sheet_names[:8]))

    text_chunks: list[str] = []
    for key, label in (
        ("text_content", "Extracted text"),
        ("text_preview", "Text preview"),
        ("info", "Info"),
        ("note", "Note"),
        ("error", "Error"),
    ):
        value = analysis.get(key) if isinstance(analysis, dict) else ""
        if isinstance(value, str) and value.strip():
            text_chunks.append(f"{label}:\n{value.strip()}")

    tables_text = analysis.get("tables_text") if isinstance(analysis, dict) else None
    if isinstance(tables_text, list) and tables_text:
        text_chunks.append("Table text:\n" + "\n".join(str(item) for item in tables_text[:8]))

    metadata = None
    if isinstance(analysis, dict):
        for key in ("metadata", "additional_info", "exif"):
            candidate = analysis.get(key)
            if isinstance(candidate, dict) and candidate:
                metadata = candidate
                break
    if metadata:
        lines.append("Metadata: " + _limit_attachment_excerpt(json.dumps(metadata, ensure_ascii=False, default=str), max_chars=800))
    if text_chunks:
        lines.append(_limit_attachment_excerpt("\n\n".join(text_chunks), max_chars=max_chars))

    return _limit_attachment_excerpt("\n".join(lines), max_chars=max_chars)


def _extract_attachment_content_from_saved_file(file_path: Path, *, max_chars: int = 6000) -> str:
    suffix = file_path.suffix.lower()
    if suffix in _EMAIL_ATTACHMENT_ANALYZER_EXTENSIONS:
        try:
            from tools.analyze_non_pdf_attachments import run as analyze_attachment

            raw_result = analyze_attachment({
                "file_path": str(file_path),
                "extract_text": True,
                "extract_metadata": True,
            })
            return _attachment_analysis_to_text(raw_result, fallback_name=file_path.name, max_chars=max_chars)
        except Exception as exc:
            fallback = extract_source(str(file_path), max_chars=max_chars)
            if fallback and not fallback.startswith("Could not extract content"):
                return _limit_attachment_excerpt(fallback, max_chars=max_chars)
            return _limit_attachment_excerpt(
                f"[ATTACHMENT: {file_path.name}]\nAttachment analysis failed: {exc}",
                max_chars=max_chars,
            )

    extracted = extract_source(str(file_path), max_chars=max_chars)
    return _limit_attachment_excerpt(extracted, max_chars=max_chars)


def _unique_attachment_path(temp_dir: Path, filename: str) -> Path:
    safe_name = _safe_attachment_filename(filename, "attachment.bin")
    candidate = temp_dir / safe_name
    stem = candidate.stem
    suffix = candidate.suffix
    counter = 1
    while candidate.exists():
        candidate = temp_dir / f"{stem}_{counter}{suffix}"
        counter += 1
    return candidate


def _save_attachment_file(temp_dir: Path, filename: str, data: bytes) -> Path:
    candidate = _unique_attachment_path(temp_dir, filename)
    candidate.write_bytes(data)
    return candidate


def _gmail_part_headers(part: dict) -> dict[str, str]:
    normalized: dict[str, str] = {}
    for header in (part.get("headers") or []):
        raw_name = str(header.get("name") or "").strip()
        if not raw_name:
            continue
        canonical_name = "-".join(piece.capitalize() for piece in raw_name.split("-"))
        normalized[canonical_name] = str(header.get("value") or "")
    return normalized


def _gmail_collect_attachments(payload: dict) -> list[dict]:
    attachments: list[dict] = []
    stack = [dict(payload or {})]
    while stack:
        part = stack.pop()
        for child in reversed(part.get("parts") or []):
            if isinstance(child, dict):
                stack.append(child)
        body = part.get("body") if isinstance(part.get("body"), dict) else {}
        filename = str(part.get("filename") or "").strip()
        attachment_id = str(body.get("attachmentId") or "").strip()
        inline_data = str(body.get("data") or "")
        mime_type = str(part.get("mimeType") or "").strip()
        disposition = str(_gmail_part_headers(part).get("Content-Disposition") or "")
        try:
            size = int(body.get("size") or 0)
        except Exception:
            size = 0
        if filename or attachment_id:
            attachments.append({
                "filename": filename,
                "attachment_id": attachment_id,
                "data": inline_data,
                "mime_type": mime_type,
                "size": size,
                "disposition": disposition,
            })
    return attachments


def _gmail_decode_body_bytes(data: str) -> bytes:
    raw = str(data or "").strip()
    if not raw:
        return b""
    padding = "=" * (-len(raw) % 4)
    try:
        return base64.urlsafe_b64decode((raw + padding).encode("utf-8"))
    except Exception:
        return b""


def _gmail_download_attachment_bytes(service, message_id: str, attachment_part: dict) -> bytes:
    inline_data = str(attachment_part.get("data") or "").strip()
    if inline_data:
        return _gmail_decode_body_bytes(inline_data)
    attachment_id = str(attachment_part.get("attachment_id") or "").strip()
    if not attachment_id:
        return b""
    payload = service.users().messages().attachments().get(
        userId="me",
        messageId=message_id,
        id=attachment_id,
    ).execute()
    return _gmail_decode_body_bytes((payload or {}).get("data", ""))