import asyncio
import re
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime

from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from starlette.responses import JSONResponse

from backend.calendar_runtime import (
    _google_delete_calendar_event,
    _google_update_calendar_event,
    _outlook_delete_calendar_event,
    _outlook_update_calendar_event,
)
from backend.config import _is_cloud_model, _load_settings, _model_ctx
from backend.mail_actions_runtime import (
    _gmail_reply_email,
    _gmail_save_draft_email,
    _outlook_reply_email,
    _outlook_save_draft_email,
)
from backend.mail_read_runtime import (
    _gmail_get_email,
    _gmail_get_thread,
    _outlook_get_email,
    _outlook_get_thread,
)
from backend.mail_render_runtime import _gmail_get_email_html, _outlook_get_email_html
from backend.models import _call_text_model_candidates, _is_llm_error, get_ollama_models


router = APIRouter()
_CHAT_FOLLOWUP_LLM_TIMEOUT_SECONDS = 90
_CHAT_FOLLOWUP_PER_MODEL_TIMEOUT_SECONDS = 18.0


class _EmailActionRequest(BaseModel):
    message_id: str
    body: str
    provider: str


class _EmailDraftProposalRequest(BaseModel):
    message_id: str
    provider: str
    model: str = ""
    tone: str = "match"
    length: str = "medium"


class _CalendarActionRequest(BaseModel):
    event_id: str
    provider: str
    summary: str = ""
    start: str = ""
    end: str = ""
    location: str = ""
    description: str = ""


def _email_draft_model_candidates(settings: dict, requested_model: str = "") -> list[str]:
    has_cloud_key = bool((settings.get("ollama_cloud_api_key") or "").strip())
    candidates: list[str] = []
    for candidate in (
        requested_model,
        settings.get("fast_model", ""),
        settings.get("main_model", ""),
        settings.get("fallback_model", ""),
    ):
        value = str(candidate or "").strip()
        if value and _is_cloud_model(value) and not has_cloud_key:
            continue
        if value and value not in candidates:
            candidates.append(value)

    installed_local: list[str] = []
    for name in get_ollama_models() or []:
        value = str(name or "").strip()
        lowered = value.lower()
        if not value or _is_cloud_model(value) or "embed" in lowered:
            continue
        installed_local.append(value)

    def _local_priority(model_name: str) -> tuple[int, str]:
        lowered = model_name.lower()
        if lowered.startswith("qwen3.6"):
            return (0, lowered)
        if lowered.startswith("qwen3"):
            return (1, lowered)
        if lowered.startswith("gemma"):
            return (2, lowered)
        return (3, lowered)

    for candidate in sorted(set(installed_local), key=_local_priority):
        if candidate not in candidates:
            candidates.append(candidate)
    return candidates


def _email_draft_per_model_timeout(candidates: list[str]) -> float:
    if any(not _is_cloud_model(candidate) for candidate in candidates):
        return 90.0
    return _CHAT_FOLLOWUP_PER_MODEL_TIMEOUT_SECONDS


def _email_draft_total_timeout(candidates: list[str]) -> float:
    if any(not _is_cloud_model(candidate) for candidate in candidates):
        return 180.0
    return float(_CHAT_FOLLOWUP_LLM_TIMEOUT_SECONDS)


def _email_draft_num_ctx(candidates: list[str], settings: dict) -> int:
    if any(_is_cloud_model(candidate) for candidate in candidates):
        return _model_ctx(8192, settings)
    return 8192


def _looks_like_email_payload(raw_email: str) -> bool:
    value = str(raw_email or "")
    if not value:
        return False
    return "Message-ID:" in value and "Provider:" in value and ("Subject:" in value or "From:" in value)


def _clean_email_draft_reply(reply: str) -> str:
    cleaned = re.sub(r'<think>[\s\S]*?</think>', '', str(reply or '')).strip()
    cleaned = cleaned.strip().strip('`').strip()
    cleaned = re.sub(r'^\s*(?:subject|onderwerp)\s*:\s.*?(?:\n+|$)', '', cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(
        r'^\s*(?:here(?: is|\'s)?|hier is)\s+(?:a\s+)?(?:concise\s+)?draft\s+reply\s*:?\s*',
        '',
        cleaned,
        flags=re.IGNORECASE,
    )
    return cleaned.strip()


_EMAIL_DRAFT_TONE_RULES = {
    "match": "Match the language and tone of the original email.",
    "warm": "Use a warm, friendly, reassuring professional tone with softer phrasing.",
    "direct": "Use a direct, crisp, efficient professional tone with minimal social filler.",
    "formal": "Use a formal, polished, businesslike tone with more structured phrasing.",
}

_EMAIL_DRAFT_LENGTH_RULES = {
    "short": "Keep it short: 2 to 3 sentences, usually under 60 words.",
    "medium": "Keep it compact: 1 practical paragraph, usually around 80 to 140 words.",
    "long": "Make it more developed: up to 2 short paragraphs, usually around 140 to 220 words.",
}


def _normalize_email_draft_tone(value: str) -> str:
    normalized = str(value or "").strip().lower()
    return normalized if normalized in _EMAIL_DRAFT_TONE_RULES else "match"


def _normalize_email_draft_length(value: str) -> str:
    normalized = str(value or "").strip().lower()
    return normalized if normalized in _EMAIL_DRAFT_LENGTH_RULES else "medium"


def _extract_email_payload_field(raw_email: str, field_name: str) -> str:
    target_prefix = f"{str(field_name or '').strip().lower()}:"
    if not target_prefix or target_prefix == ":":
        return ""
    for line in str(raw_email or "").splitlines():
        if line.lower().startswith(target_prefix):
            return line.split(":", 1)[1].strip()
    return ""


def _extract_email_payload_body(raw_email: str) -> str:
    header_fields = {"subject", "from", "to", "cc", "date", "attachments", "message-id", "message id", "thread id", "provider"}
    lines = str(raw_email or "").splitlines()
    body_started = False
    body_lines: list[str] = []
    for line in lines:
        if not body_started:
            stripped = line.strip()
            if not stripped:
                body_started = True
                continue
            if ":" in stripped:
                key = stripped.split(":", 1)[0].strip().lower()
                if key in header_fields:
                    continue
            body_started = True
        if body_started:
            body_lines.append(line)
    return "\n".join(body_lines).strip()


def _trim_email_body_for_draft(text: str, limit: int = 2200) -> str:
    value = str(text or "").strip()
    if not value:
        return ""
    separator_match = re.search(
        r"(?im)^(?:-{2,}\s*original message\s*-{2,}|from:\s|van:\s|sent:\s|verzonden:\s|subject:\s|onderwerp:\s|on .+ wrote:\s*$|op .+ schreef .+:\s*$)",
        value,
    )
    if separator_match:
        value = value[:separator_match.start()].strip()
    value = re.sub(r"\n{3,}", "\n\n", value)
    return value[:max(400, int(limit or 2200))].strip()


def _compact_email_draft_prompt_source(raw_email: str, thread_context: str = "", latest_message: str = "") -> str:
    subject = _extract_email_payload_field(raw_email, "Subject")
    sender = _extract_email_payload_field(raw_email, "From")
    date_value = _extract_email_payload_field(raw_email, "Date")
    latest_body = ""
    if latest_message:
        latest_body = _trim_email_body_for_draft(_extract_email_payload_field(latest_message, "Body"), limit=2200)
    if not latest_body:
        latest_body = _trim_email_body_for_draft(_extract_email_payload_body(raw_email), limit=2200)
    earlier_context = _trim_email_body_for_draft(thread_context, limit=900)

    parts = [
        "[EMAIL TO REPLY TO]",
        f"From: {sender or '?'}",
        f"Subject: {subject or '(no subject)'}",
    ]
    if date_value:
        parts.append(f"Date: {date_value}")
    parts.append("Message:")
    parts.append(latest_body or "(No body text available.)")
    if earlier_context:
        parts.append("")
        parts.append("[OPTIONAL EARLIER CONTEXT]")
        parts.append(earlier_context)
    return "\n".join(parts).strip()


def _parse_thread_message_datetime(value: str) -> datetime | None:
    text = str(value or "").strip()
    if not text or text == "?":
        return None
    try:
        parsed = parsedate_to_datetime(text)
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        return parsed.astimezone(timezone.utc)
    except Exception:
        pass
    normalized = text.replace("Z", "+00:00")
    for fmt in (
        None,
        "%Y-%m-%d %H:%M:%S",
        "%m/%d/%Y %H:%M:%S",
        "%m/%d/%Y %I:%M:%S %p",
    ):
        try:
            parsed = datetime.fromisoformat(normalized) if fmt is None else datetime.strptime(text, fmt)
            if parsed.tzinfo is None:
                parsed = parsed.replace(tzinfo=timezone.utc)
            return parsed.astimezone(timezone.utc)
        except Exception:
            continue
    return None


def _parse_thread_messages(raw_thread: str) -> list[dict[str, object]]:
    messages: list[dict[str, object]] = []
    for index, block in enumerate(re.split(r"\n\s*---\s*\n", str(raw_thread or "").strip())):
        chunk = str(block or "").strip()
        if not chunk:
            continue
        lines = chunk.splitlines()
        from_value = ""
        date_value = ""
        body_lines: list[str] = []
        body_started = False
        for line in lines:
            if line.startswith("From:"):
                from_value = line.split(":", 1)[1].strip()
            elif line.startswith("Date:"):
                date_value = line.split(":", 1)[1].strip()
            elif line.startswith("Body:"):
                body_started = True
                body_lines.append(line.split(":", 1)[1].strip())
            elif body_started:
                body_lines.append(line)
        body_value = "\n".join(body_lines).strip()
        parsed_date = _parse_thread_message_datetime(date_value)
        if not from_value and not body_value:
            continue
        messages.append(
            {
                "index": index,
                "from": from_value,
                "date": date_value,
                "body": body_value,
                "parsed_date": parsed_date,
            }
        )
    return messages


def _format_thread_message(message: dict[str, object]) -> str:
    lines = []
    from_value = str(message.get("from") or "").strip()
    date_value = str(message.get("date") or "").strip()
    body_value = str(message.get("body") or "").strip()
    if from_value:
        lines.append(f"From: {from_value}")
    if date_value:
        lines.append(f"Date: {date_value}")
    lines.append(f"Body: {body_value}" if body_value else "Body: ")
    return "\n".join(lines).strip()


async def _resolve_thread_prompt_sections(provider: str, raw_email: str) -> tuple[str, str]:
    thread_id = _extract_email_payload_field(raw_email, "Thread ID")
    if not thread_id or thread_id == "?":
        return "", ""

    loop = asyncio.get_event_loop()
    if provider == "outlook":
        raw_thread = await loop.run_in_executor(None, _outlook_get_thread, thread_id, 1200, 8)
    else:
        raw_thread = await loop.run_in_executor(None, _gmail_get_thread, thread_id, 1200, 8)

    thread_messages = [
        message
        for message in _parse_thread_messages(raw_thread)
        if message.get("parsed_date") is not None
    ]
    if not thread_messages:
        return "", ""

    thread_messages.sort(key=lambda item: (item["parsed_date"], int(item.get("index") or 0)))
    latest_message = thread_messages[-1]
    historical_messages = thread_messages[:-1][-4:]

    thread_context_parts: list[str] = []
    omitted_count = max(0, len(thread_messages) - 1 - len(historical_messages))
    if omitted_count:
        thread_context_parts.append(f"[{omitted_count} older thread message(s) omitted]")
    for idx, message in enumerate(historical_messages, start=1):
        thread_context_parts.append(f"[Earlier message {idx}]\n{_format_thread_message(message)}")

    return "\n\n".join(thread_context_parts).strip(), _format_thread_message(latest_message)


def _build_email_draft_prompt(
    raw_email: str,
    *,
    tone: str,
    length: str,
    thread_context: str = "",
    latest_message: str = "",
    compact: bool = False,
) -> str:
    normalized_tone = _normalize_email_draft_tone(tone)
    normalized_length = _normalize_email_draft_length(length)
    tone_instruction = _EMAIL_DRAFT_TONE_RULES[normalized_tone]
    length_instruction = _EMAIL_DRAFT_LENGTH_RULES[normalized_length]
    prompt = (
        "Write a reply draft to the email below.\n"
        "Rules:\n"
        f"- Tone setting: {normalized_tone}. {tone_instruction}\n"
        f"- Length setting: {normalized_length}. {length_instruction}\n"
        "- The selected tone and length are mandatory. Make the output clearly reflect them.\n"
        "- If the tone is not 'match', follow the selected tone even if the original email sounds different.\n"
        "- Use the thread context to resolve references, prior commitments, and unanswered questions before drafting.\n"
        "- Reply to the latest actionable message, not to older quoted text, unless the newest message clearly depends on it.\n"
        "- If the sender asked for a decision, confirmation, deliverable, timing update, or next step, address that directly instead of sending a generic acknowledgement.\n"
        "- Preserve concrete names, dates, promises, and requested actions when they matter to the reply.\n"
        "- Write like a capable human assistant: natural, specific, and ready to send.\n"
        "- Return ONLY the reply body text.\n"
        "- Do not include a subject line.\n"
        "- Do not include headings, explanations, or meta-commentary.\n"
        "- If the sender asked questions, answer them clearly when possible.\n"
        "- End naturally without placeholders or bracketed notes.\n"
    )
    if compact:
        prompt += (
            "- Focus on the newest actionable message only. Ignore quoted history unless it is clearly needed.\n"
            "- Prefer a concise, directly usable reply over perfect completeness.\n\n"
            f"{_compact_email_draft_prompt_source(raw_email, thread_context=thread_context, latest_message=latest_message)}\n"
        )
    elif latest_message:
        prompt += (
            "- Treat THREAD CONTEXT as background only; write the reply to the LATEST MESSAGE TO REPLY TO.\n\n"
            "[THREAD CONTEXT]\n"
            f"{str(thread_context or '(No earlier thread context available.)').strip()[:16000]}\n\n"
            "[LATEST MESSAGE TO REPLY TO]\n"
            f"{str(latest_message or '').strip()[:12000]}\n"
        )
    else:
        prompt += (
            "\n[ORIGINAL EMAIL]\n"
            f"{str(raw_email or '').strip()[:30000]}\n"
        )
    return prompt


@router.get("/api/email-html")
async def api_email_html(id: str = Query(""), source: str = Query("")):
    if not id:
        raise HTTPException(status_code=400, detail="Missing id parameter")
    src = source.strip().lower()
    if src not in {"outlook", "gmail"}:
        raise HTTPException(status_code=400, detail="source must be 'outlook' or 'gmail'")
    loop = asyncio.get_event_loop()
    if src == "outlook":
        html = await loop.run_in_executor(None, _outlook_get_email_html, id)
    else:
        html = await loop.run_in_executor(None, _gmail_get_email_html, id)
    if not html:
        html = "<html><body style='font-family:sans-serif;color:#888'>Unable to load email HTML.</body></html>"
    return HTMLResponse(content=html, media_type="text/html; charset=utf-8")


@router.post("/api/email-reply")
async def api_email_reply(req: _EmailActionRequest):
    mid = str(req.message_id or "").strip()
    body = str(req.body or "").strip()
    provider = str(req.provider or "").strip().lower()
    if not mid or not body:
        raise HTTPException(status_code=400, detail="message_id and body are required")
    loop = asyncio.get_event_loop()
    if provider == "outlook":
        result = await loop.run_in_executor(None, _outlook_reply_email, mid, body)
    elif provider == "gmail":
        result = await loop.run_in_executor(None, _gmail_reply_email, mid, body)
    else:
        raise HTTPException(status_code=400, detail="provider must be 'outlook' or 'gmail'")
    ok = not result.lower().startswith("error") and "error" not in result.lower()
    return {"ok": ok, "message": result}


@router.post("/api/email-draft")
async def api_email_draft(req: _EmailActionRequest):
    mid = str(req.message_id or "").strip()
    body = str(req.body or "").strip()
    provider = str(req.provider or "").strip().lower()
    if not mid or not body:
        raise HTTPException(status_code=400, detail="message_id and body are required")
    loop = asyncio.get_event_loop()
    if provider == "outlook":
        result = await loop.run_in_executor(None, _outlook_save_draft_email, mid, body)
    elif provider == "gmail":
        result = await loop.run_in_executor(None, _gmail_save_draft_email, mid, body)
    else:
        raise HTTPException(status_code=400, detail="provider must be 'outlook' or 'gmail'")
    ok = not result.lower().startswith("error") and "error" not in result.lower()
    return {"ok": ok, "message": result}


@router.post("/api/email-propose-reply")
async def api_email_propose_reply(req: _EmailDraftProposalRequest):
    mid = str(req.message_id or "").strip()
    provider = str(req.provider or "").strip().lower()
    tone = _normalize_email_draft_tone(req.tone)
    length = _normalize_email_draft_length(req.length)
    if not mid:
        raise HTTPException(status_code=400, detail="message_id is required")
    if provider not in {"outlook", "gmail"}:
        raise HTTPException(status_code=400, detail="provider must be 'outlook' or 'gmail'")

    settings = _load_settings()
    draft_models = _email_draft_model_candidates(settings, req.model)
    selected_model = draft_models[0] if draft_models else ""
    if not draft_models:
        return JSONResponse(
            {"ok": False, "error": "LLM not available — no draft model is configured."},
            status_code=503,
        )

    loop = asyncio.get_event_loop()
    if provider == "outlook":
        raw_email = await loop.run_in_executor(None, _outlook_get_email, mid)
    else:
        raw_email = await loop.run_in_executor(None, _gmail_get_email, mid)

    if not _looks_like_email_payload(raw_email):
        return JSONResponse(
            {"ok": False, "error": str(raw_email or "Could not load the selected email.")},
            status_code=400,
        )

    thread_context, latest_message = await _resolve_thread_prompt_sections(provider, raw_email)

    prompt = _build_email_draft_prompt(
        raw_email,
        tone=tone,
        length=length,
        thread_context=thread_context,
        latest_message=latest_message,
        compact=False,
    )

    per_model_timeout = _email_draft_per_model_timeout(draft_models)
    total_timeout = _email_draft_total_timeout(draft_models)
    draft_num_ctx = _email_draft_num_ctx(draft_models, settings)

    try:
        reply, used_model = await asyncio.wait_for(
            _call_text_model_candidates(
                prompt,
                draft_models,
                system=(
                    "You write high-quality email replies. "
                    "Use the latest message as the reply target, use thread context for accuracy, "
                    "and obey the requested tone and length exactly. Return only the reply body text."
                ),
                num_ctx=draft_num_ctx,
                thinking=False,
                per_model_timeout=per_model_timeout,
            ),
            timeout=total_timeout,
        )
    except asyncio.TimeoutError:
        return JSONResponse(
            {
                "ok": False,
                "error": "LLM draft generation timed out before a reply was produced.",
                "tone": tone,
                "length": length,
            },
            status_code=503,
        )
    except Exception as exc:
        return JSONResponse(
            {
                "ok": False,
                "error": f"LLM draft generation failed: {exc}",
                "tone": tone,
                "length": length,
            },
            status_code=503,
        )

    cleaned_reply = _clean_email_draft_reply(reply)
    effective_model = str(used_model or selected_model).strip() or selected_model
    if not cleaned_reply or _is_llm_error(cleaned_reply):
        return JSONResponse(
            {
                "ok": False,
                "error": cleaned_reply or "LLM draft generation did not produce a usable reply.",
                "tone": tone,
                "length": length,
            },
            status_code=503,
        )

    return {"ok": True, "draft": cleaned_reply, "model": effective_model, "tone": tone, "length": length}


@router.post("/api/calendar-delete")
async def api_calendar_delete(req: _CalendarActionRequest):
    event_id = str(req.event_id or "").strip()
    provider = str(req.provider or "").strip().lower()
    if not event_id:
        raise HTTPException(status_code=400, detail="event_id is required")
    loop = asyncio.get_event_loop()
    if provider == "outlook":
        result = await loop.run_in_executor(None, _outlook_delete_calendar_event, event_id)
    elif provider == "google":
        result = await loop.run_in_executor(None, _google_delete_calendar_event, event_id)
    else:
        raise HTTPException(status_code=400, detail="provider must be 'outlook' or 'google'")
    ok = not result.lower().startswith("error") and "error" not in result.lower()
    return {"ok": ok, "message": result}


@router.post("/api/calendar-update")
async def api_calendar_update(req: _CalendarActionRequest):
    event_id = str(req.event_id or "").strip()
    provider = str(req.provider or "").strip().lower()
    if not event_id:
        raise HTTPException(status_code=400, detail="event_id is required")
    loop = asyncio.get_event_loop()
    summary = str(req.summary or "").strip()
    start = str(req.start or "").strip()
    end = str(req.end or "").strip()
    location = str(req.location or "").strip()
    description = str(req.description or "").strip()
    if provider == "outlook":
        result = await loop.run_in_executor(
            None,
            lambda: _outlook_update_calendar_event(
                event_id=event_id, summary=summary, start=start,
                end=end, location=location, description=description,
            ),
        )
    elif provider == "google":
        result = await loop.run_in_executor(
            None,
            lambda: _google_update_calendar_event(
                event_id=event_id, summary=summary, start=start,
                end=end, location=location, description=description,
            ),
        )
    else:
        raise HTTPException(status_code=400, detail="provider must be 'outlook' or 'google'")
    ok = not result.lower().startswith("error") and "error" not in result.lower()
    return {"ok": ok, "message": result}