"""Workspace-scoped Outlook helpers for the web UI."""

from __future__ import annotations

import asyncio
import json
import os
import shlex
from contextlib import AsyncExitStack
from dataclasses import asdict, dataclass
from datetime import datetime, time, timedelta
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo

import httpx

from beaver.foundation.config import BeaverConfig
from beaver.integrations.authz import AuthzClient


OUTLOOK_SERVER_ID = os.getenv("BEAVER_OUTLOOK_MCP_SERVER_ID", "outlook_mcp")
OUTLOOK_OVERVIEW_MESSAGE_LIMIT = 8
OUTLOOK_OVERVIEW_EVENT_LIMIT = 20
OUTLOOK_MAX_PAGE_SIZE = 100


class OutlookIntegrationError(RuntimeError):
    """Raised when the Outlook integration backend is unavailable or misconfigured."""


@dataclass(frozen=True)
class OutlookDefaults:
    domain: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_DOMAIN", "")
    service_endpoint: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_EWS_URL", "")
    server: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_EWS_SERVER", "")
    default_timezone: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_TIMEZONE", "Asia/Shanghai")
    autodiscover: bool = os.getenv("BEAVER_OUTLOOK_DEFAULT_AUTODISCOVER", "0") == "1"


@dataclass(frozen=True)
class OutlookConnectionInput:
    email: str
    password: str
    username: str | None = None
    domain: str | None = None
    service_endpoint: str | None = None
    server: str | None = None
    autodiscover: bool = False
    default_timezone: str = "Asia/Shanghai"


OUTLOOK_TOOL_NAMES = [
    "auth_status",
    "mail_list_folders",
    "mail_list_messages",
    "mail_search_messages",
    "mail_get_message",
    "mail_send_email",
    "mail_reply_to_message",
    "mail_forward_message",
    "mail_move_message",
    "mail_delta_sync",
    "calendar_list_events",
    "calendar_create_event",
    "calendar_update_event",
    "calendar_get_schedule",
    "calendar_find_meeting_times",
    "calendar_delta_sync",
]


def _call_timeout_seconds() -> float:
    raw = os.getenv("BEAVER_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS", "").strip()
    try:
        return max(1.0, float(raw)) if raw else 10.0
    except ValueError:
        return 10.0


def _use_authz_mode(config: BeaverConfig) -> bool:
    return bool(config.authz.enabled and config.authz.base_url.strip())


def _authz_client(config: BeaverConfig) -> AuthzClient:
    if not _use_authz_mode(config):
        raise OutlookIntegrationError("AuthZ mode is not enabled.")
    return AuthzClient(config.authz.base_url, timeout_seconds=int(config.authz.request_timeout_seconds))


def _require_backend_identity(config: BeaverConfig) -> str:
    backend_id = config.backend_identity.backend_id.strip()
    client_id = config.backend_identity.client_id.strip()
    client_secret = config.backend_identity.client_secret.strip()
    if not (backend_id and client_id and client_secret):
        raise OutlookIntegrationError("Backend is not registered with AuthZ yet.")
    return backend_id


def _outlook_mcp_url(config: BeaverConfig) -> str:
    url = config.authz.outlook_mcp_url.strip()
    if not url:
        raise OutlookIntegrationError("AuthZ mode requires authz.outlook_mcp_url to be configured.")
    return url


def outlook_defaults() -> dict[str, Any]:
    return {
        "provider": "ews",
        "server_id": OUTLOOK_SERVER_ID,
        "mcp_command": os.getenv("BEAVER_OUTLOOK_MCP_COMMAND", "bw-outlook-mcp"),
        "mcp_extra_args": shlex.split(os.getenv("BEAVER_OUTLOOK_MCP_EXTRA_ARGS", "").strip()),
        "fields": asdict(OutlookDefaults()),
    }


def outlook_mcp_config_payload(config: BeaverConfig) -> dict[str, Any]:
    url = _outlook_mcp_url(config)
    return {
        "url": url,
        "authMode": "oauth_backend_token",
        "authAudience": f"mcp:{OUTLOOK_SERVER_ID}",
        "authScopes": ["list_tools", *[f"tool:{name}" for name in OUTLOOK_TOOL_NAMES]],
        "sensitive": True,
        "toolTimeout": 60,
        "kind": "online",
        "category": "outlook",
        "managed": True,
        "displayName": "Outlook MCP",
        "source": "beaver-managed",
    }


def _meta_file(workspace: Path) -> Path:
    return workspace.expanduser().resolve() / "state" / "bw_outlook_mcp" / "ui_meta.json"


def _load_meta(workspace: Path) -> dict[str, Any]:
    path = _meta_file(workspace)
    if not path.exists():
        return {}
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError, ValueError):
        return {}
    return data if isinstance(data, dict) else {}


def _update_meta(workspace: Path, **fields: Any) -> dict[str, Any]:
    payload = _load_meta(workspace)
    payload.update(fields)
    payload["updated_at"] = datetime.now().isoformat()
    path = _meta_file(workspace)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    return payload


def _normalize_input(data: OutlookConnectionInput) -> OutlookConnectionInput:
    email = data.email.strip()
    password = data.password
    username = (data.username or "").strip() or email.partition("@")[0].strip()
    domain = (data.domain or "").strip() or None
    service_endpoint = (data.service_endpoint or "").strip() or None
    server = (data.server or "").strip() or None
    default_timezone = (data.default_timezone or "").strip() or OutlookDefaults.default_timezone
    if service_endpoint:
        server = None
    if not email:
        raise OutlookIntegrationError("Email is required.")
    if not password:
        raise OutlookIntegrationError("Password is required.")
    if not username:
        raise OutlookIntegrationError("Username is required.")
    if not data.autodiscover and not service_endpoint and not server:
        raise OutlookIntegrationError("Provide an EWS URL, a server host, or enable autodiscover.")
    return OutlookConnectionInput(
        email=email,
        password=password,
        username=username,
        domain=domain,
        service_endpoint=service_endpoint,
        server=server,
        autodiscover=bool(data.autodiscover),
        default_timezone=default_timezone,
    )


def _default_outlook_permissions() -> dict[str, Any]:
    return {
        "mcp": {
            OUTLOOK_SERVER_ID: {
                "enabled": True,
                "tools": list(OUTLOOK_TOOL_NAMES),
            }
        },
        "a2a": {"enabled": False, "agents": []},
    }


async def ensure_outlook_authz_permissions(config: BeaverConfig) -> None:
    backend_id = _require_backend_identity(config)
    client = _authz_client(config)
    existing = await client.get_permissions(backend_id)
    mcp_settings = existing.get("mcp", {}).get(OUTLOOK_SERVER_ID, {}) if isinstance(existing, dict) else {}
    if isinstance(mcp_settings, dict) and mcp_settings.get("enabled"):
        return
    await client.set_permissions(backend_id, _default_outlook_permissions())


async def _call_outlook_mcp_tool(
    config: BeaverConfig,
    tool_name: str,
    arguments: dict[str, Any],
    *,
    scopes: list[str] | None = None,
    timeout_seconds: float | None = None,
) -> dict[str, Any]:
    from mcp import ClientSession, types
    from mcp.client.streamable_http import streamable_http_client

    url = _outlook_mcp_url(config)
    client = _authz_client(config)
    try:
        token_response = await client.issue_token(
            client_id=config.backend_identity.client_id,
            client_secret=config.backend_identity.client_secret,
            audience=f"mcp:{OUTLOOK_SERVER_ID}",
            scopes=scopes or ["list_tools", f"tool:{tool_name}"],
        )
    except httpx.TimeoutException as exc:
        raise OutlookIntegrationError("AuthZ token 请求超时。") from exc
    except httpx.HTTPError as exc:
        detail = str(exc).strip() or exc.__class__.__name__
        raise OutlookIntegrationError(f"AuthZ token 获取失败：{detail}") from exc

    access_token = str(token_response.get("access_token") or "").strip()
    if not access_token:
        raise OutlookIntegrationError("Failed to obtain an Outlook MCP access token.")

    async def _invoke() -> dict[str, Any]:
        async with AsyncExitStack() as stack:
            http_client = await stack.enter_async_context(
                httpx.AsyncClient(
                    headers={"Authorization": f"Bearer {access_token}"},
                    follow_redirects=True,
                    trust_env=False,
                    timeout=timeout_seconds or _call_timeout_seconds(),
                )
            )
            read, write, _ = await stack.enter_async_context(streamable_http_client(url, http_client=http_client))
            session = await stack.enter_async_context(ClientSession(read, write))
            await session.initialize()
            result = await session.call_tool(tool_name, arguments=arguments)
        parts: list[str] = []
        for block in result.content:
            parts.append(block.text if isinstance(block, types.TextContent) else str(block))
        output = "\n".join(parts).strip()
        if not output:
            return {}
        try:
            parsed = json.loads(output)
        except json.JSONDecodeError:
            return {"text": output}
        return parsed if isinstance(parsed, dict) else {"value": parsed}

    timeout_value = timeout_seconds or _call_timeout_seconds()
    try:
        return await asyncio.wait_for(_invoke(), timeout=timeout_value)
    except TimeoutError as exc:
        raise OutlookIntegrationError(f"Outlook MCP 请求超时：{tool_name} 超过 {int(timeout_value)}s") from exc
    except OutlookIntegrationError:
        raise
    except Exception as exc:
        detail = str(exc).strip() or exc.__class__.__name__
        raise OutlookIntegrationError(f"Outlook MCP 调用失败：{detail}") from exc


async def test_connection(data: OutlookConnectionInput, config: BeaverConfig) -> dict[str, Any]:
    if not _use_authz_mode(config):
        raise OutlookIntegrationError("Outlook setup requires AuthZ mode in this Beaver instance.")
    normalized = _normalize_input(data)
    return {
        "ok": True,
        "provider": "ews",
        "mailbox": normalized.email,
        "resolved_username": normalized.username or "",
        "resolved_domain": normalized.domain,
        "sample": {"folders": [], "inbox": [], "events": []},
        "warnings": [
            "AuthZ mode skips local EWS validation. Credentials will be validated by the Outlook MCP service after save."
        ],
    }


async def connect_workspace(config: BeaverConfig, workspace: Path, data: OutlookConnectionInput) -> dict[str, Any]:
    probe = await test_connection(data, config)
    normalized = _normalize_input(data)
    backend_id = _require_backend_identity(config)
    client = _authz_client(config)
    await client.set_outlook_settings(
        backend_id,
        {
            "configured": True,
            "email": normalized.email,
            "username": normalized.username,
            "domain": normalized.domain,
            "service_endpoint": normalized.service_endpoint,
            "server": normalized.server,
            "autodiscover": normalized.autodiscover,
            "default_timezone": normalized.default_timezone,
            "password": normalized.password,
        },
    )
    await ensure_outlook_authz_permissions(config)
    meta = _update_meta(
        workspace,
        provider="ews",
        mailbox=normalized.email,
        last_verified_at=datetime.now().isoformat(),
        last_connected_at=datetime.now().isoformat(),
    )
    return {
        "ok": True,
        "probe": probe["sample"],
        "saved": {"backend_id": backend_id, "configured": True},
        "mcp": {"id": OUTLOOK_SERVER_ID, **outlook_mcp_config_payload(config)},
        "meta": meta,
    }


async def disconnect_workspace(config: BeaverConfig) -> dict[str, Any]:
    backend_id = _require_backend_identity(config)
    removed = False
    try:
        result = await _authz_client(config).delete_outlook_settings(backend_id)
        removed = bool(result.get("ok"))
    except Exception:
        removed = False
    return {"ok": True, "removed_state": removed, "removed_mcp": False, "server_id": OUTLOOK_SERVER_ID}


async def outlook_status(config: BeaverConfig, workspace: Path) -> dict[str, Any]:
    meta = _load_meta(workspace)
    if not _use_authz_mode(config):
        return {
            "configured": False,
            "connected": False,
            "provider": None,
            "storage_mode": "workspace",
            "saved": None,
            "auth_status": None,
            "mcp_registered": OUTLOOK_SERVER_ID in config.tools.mcp_servers,
            "mcp_server_id": OUTLOOK_SERVER_ID,
            "defaults": outlook_defaults(),
            "meta": meta,
            "error": "Outlook setup requires AuthZ mode in this Beaver instance.",
        }

    client = _authz_client(config)
    backend_id = _require_backend_identity(config)
    saved = await client.get_outlook_settings(backend_id)
    configured = bool(saved.get("configured"))
    connected = False
    auth_status: dict[str, Any] | None = None
    error: str | None = None
    if configured:
        try:
            auth_status = await _call_outlook_mcp_tool(config, "auth_status", {}, scopes=["list_tools", "tool:auth_status"])
            connected = bool(auth_status.get("authenticated"))
        except Exception as exc:
            error = str(exc)
    return {
        "configured": configured,
        "connected": connected,
        "provider": "ews" if configured else None,
        "storage_mode": "authz",
        "saved": saved if configured else None,
        "auth_status": auth_status,
        "mcp_registered": bool(OUTLOOK_SERVER_ID in config.tools.mcp_servers or config.authz.outlook_mcp_url.strip()),
        "mcp_server_id": OUTLOOK_SERVER_ID,
        "defaults": outlook_defaults(),
        "meta": meta,
        "error": error,
    }


async def get_overview(config: BeaverConfig, workspace: Path) -> dict[str, Any]:
    saved = await _authz_client(config).get_outlook_settings(_require_backend_identity(config))
    if not saved.get("configured"):
        raise OutlookIntegrationError("Outlook is not configured for this backend.")
    timezone_name = str(saved.get("default_timezone") or "Asia/Shanghai")
    now = datetime.now(ZoneInfo(timezone_name))
    start_of_day = datetime.combine(now.date(), time.min, tzinfo=now.tzinfo)
    end_of_day = start_of_day + timedelta(days=1)
    warnings: list[str] = []

    async def _load_section(label: str, coro: Any) -> dict[str, Any]:
        try:
            payload = await coro
            return payload if isinstance(payload, dict) else {"value": []}
        except Exception as exc:
            warnings.append(f"{label} unavailable: {exc}")
            return {"value": []}

    inbox, sent, calendar = await asyncio.gather(
        _load_section(
            "inbox",
            _call_outlook_mcp_tool(
                config,
                "mail_list_messages",
                {"folder": "inbox", "top": OUTLOOK_OVERVIEW_MESSAGE_LIMIT, "skip": 0},
                scopes=["list_tools", "tool:mail_list_messages"],
            ),
        ),
        _load_section(
            "sent items",
            _call_outlook_mcp_tool(
                config,
                "mail_list_messages",
                {"folder": "sentitems", "top": OUTLOOK_OVERVIEW_MESSAGE_LIMIT, "skip": 0},
                scopes=["list_tools", "tool:mail_list_messages"],
            ),
        ),
        _load_section(
            "calendar",
            _call_outlook_mcp_tool(
                config,
                "calendar_list_events",
                {
                    "start_time": start_of_day.isoformat(),
                    "end_time": end_of_day.isoformat(),
                    "top": OUTLOOK_OVERVIEW_EVENT_LIMIT,
                    "skip": 0,
                },
                scopes=["list_tools", "tool:calendar_list_events"],
            ),
        ),
    )
    meta = _update_meta(workspace, last_overview_refresh_at=datetime.now().isoformat())
    return {
        "mailbox": saved.get("email") or "",
        "timezone": timezone_name,
        "today": now.date().isoformat(),
        "connection": await outlook_status(config, workspace),
        "recentInbox": inbox.get("value", []),
        "recentSent": sent.get("value", []),
        "todayEvents": calendar.get("value", []),
        "warnings": warnings,
        "meta": meta,
    }


def _normalize_page_args(*, top: int, skip: int) -> tuple[int, int]:
    return max(1, min(int(top), OUTLOOK_MAX_PAGE_SIZE)), max(0, int(skip))


def _normalize_page_payload(payload: dict[str, Any], *, top: int, skip: int) -> dict[str, Any]:
    items = payload.get("value", []) if isinstance(payload, dict) else []
    returned = len(items) if isinstance(items, list) else 0
    page = payload.get("page") if isinstance(payload, dict) else None
    if isinstance(page, dict):
        return {
            **payload,
            "page": {
                "top": int(page.get("top", top)),
                "skip": int(page.get("skip", skip)),
                "returned": int(page.get("returned", returned)),
                "has_more": bool(page.get("has_more", False)),
                "next_skip": page.get("next_skip"),
            },
        }
    return {
        **payload,
        "page": {
            "top": top,
            "skip": skip,
            "returned": returned,
            "has_more": returned >= top,
            "next_skip": skip + returned if returned >= top else None,
        },
    }


async def list_messages(
    config: BeaverConfig,
    *,
    folder: str,
    top: int,
    skip: int = 0,
    unread_only: bool = False,
) -> dict[str, Any]:
    safe_top, safe_skip = _normalize_page_args(top=top, skip=skip)
    payload = await _call_outlook_mcp_tool(
        config,
        "mail_list_messages",
        {"folder": folder, "top": safe_top, "skip": safe_skip, "unread_only": unread_only},
        scopes=["list_tools", "tool:mail_list_messages"],
    )
    return {"folder": folder, "unread_only": unread_only, **_normalize_page_payload(payload, top=safe_top, skip=safe_skip)}


async def list_events(
    config: BeaverConfig,
    *,
    start_time: str,
    end_time: str,
    top: int,
    skip: int = 0,
) -> dict[str, Any]:
    safe_top, safe_skip = _normalize_page_args(top=top, skip=skip)
    payload = await _call_outlook_mcp_tool(
        config,
        "calendar_list_events",
        {"start_time": start_time, "end_time": end_time, "top": safe_top, "skip": safe_skip},
        scopes=["list_tools", "tool:calendar_list_events"],
    )
    return {"start_time": start_time, "end_time": end_time, **_normalize_page_payload(payload, top=safe_top, skip=safe_skip)}


async def get_message_detail(config: BeaverConfig, message_id: str, *, changekey: str | None = None) -> dict[str, Any]:
    return await _call_outlook_mcp_tool(
        config,
        "mail_get_message",
        {"message_id": message_id, "changekey": changekey},
        scopes=["list_tools", "tool:mail_get_message"],
    )
