from __future__ import annotations

import base64
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any

from tools.registry import registry

_TOOLSET = "messaging"
GRAPH = "https://graph.microsoft.com/v1.0"
CLIENT_ID = os.getenv("OUTLOOK_CLIENT_ID", "e215cedb-b6e8-466a-a6e7-334f12af748f")
AUTH = os.getenv("OUTLOOK_AUTHORITY", "https://login.microsoftonline.com/consumers").rstrip("/")
SCOPES = os.getenv(
    "OUTLOOK_SCOPES",
    "User.Read Mail.Read Mail.ReadWrite Mail.Send Contacts.Read Contacts.ReadWrite "
    "Calendars.Read Calendars.ReadWrite Files.Read People.Read offline_access",
)
CACHE = Path(os.getenv("OUTLOOK_TOKEN_CACHE", "/opt/data/outlook_graph_token_cache.json"))
ATTACH_DIR = Path(os.getenv("OUTLOOK_ATTACH_DIR", "/opt/data/attachments/outlook"))
FILES_DIR = Path(os.getenv("OUTLOOK_FILES_DIR", "/opt/data/onedrive"))


def out(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False)


def confirm_required(args: dict[str, Any], action: str) -> str | None:
    if args.get("confirm") is True:
        return None
    return out({"error": "confirmation_required", "action": action, "hint": "Repeat with confirm:true"})


def cache_read() -> dict[str, Any]:
    try:
        return json.loads(CACHE.read_text()) if CACHE.exists() else {}
    except Exception:
        return {}


def cache_write(value: dict[str, Any]) -> None:
    CACHE.parent.mkdir(parents=True, exist_ok=True)
    CACHE.write_text(json.dumps(value, indent=2))
    try:
        CACHE.chmod(0o600)
    except OSError:
        pass


def request_json(method: str, url: str, access_token: str | None = None, data: Any = None, form: dict[str, Any] | None = None, raw: bool = False) -> Any:
    headers = {"Accept": "application/json"}
    body = None
    if access_token:
        headers["Authorization"] = "Bearer " + access_token
    if data is not None:
        headers["Content-Type"] = "application/json"
        body = json.dumps(data).encode()
    if form is not None:
        headers["Content-Type"] = "application/x-www-form-urlencoded"
        body = urllib.parse.urlencode(form).encode()

    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=45) as response:
            payload = response.read()
            if raw:
                return payload
            return json.loads(payload.decode()) if payload else {}
    except urllib.error.HTTPError as exc:
        payload = exc.read().decode(errors="replace")
        try:
            error = json.loads(payload)
        except Exception:
            error = {"error": payload}
        raise RuntimeError(out({"status": exc.code, "error": error}))


def stamp(token_payload: dict[str, Any]) -> dict[str, Any]:
    token_payload["obtained_at"] = int(time.time())
    token_payload["expires_at"] = token_payload["obtained_at"] + int(token_payload.get("expires_in", 3600)) - 60
    return token_payload


def refresh_token(token_payload: dict[str, Any]) -> dict[str, Any] | None:
    refresh = token_payload.get("refresh_token")
    if not refresh:
        return None
    return stamp(
        request_json(
            "POST",
            AUTH + "/oauth2/v2.0/token",
            form={
                "client_id": CLIENT_ID,
                "grant_type": "refresh_token",
                "refresh_token": refresh,
                "scope": SCOPES,
            },
        )
    )


def auth() -> str:
    flow = request_json(
        "POST",
        AUTH + "/oauth2/v2.0/devicecode",
        form={"client_id": CLIENT_ID, "scope": SCOPES},
    )
    expires_at = time.time() + int(flow.get("expires_in", 900))
    interval = int(flow.get("interval", 5))
    print(
        out(
            {
                "verification_uri": flow.get("verification_uri"),
                "user_code": flow.get("user_code"),
                "message": flow.get("message"),
                "expires_in": flow.get("expires_in"),
                "status": "waiting_for_user",
            }
        ),
        flush=True,
    )
    while time.time() < expires_at:
        time.sleep(interval)
        try:
            token_payload = request_json(
                "POST",
                AUTH + "/oauth2/v2.0/token",
                form={
                    "client_id": CLIENT_ID,
                    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
                    "device_code": flow["device_code"],
                },
            )
            token_payload = stamp(token_payload)
            cache_write(token_payload)
            return out({"ok": True, "cached_to": str(CACHE), "scopes": SCOPES})
        except RuntimeError as exc:
            try:
                payload = json.loads(str(exc)).get("error", {})
                if isinstance(payload, dict):
                    nested = payload.get("error")
                    if isinstance(nested, str):
                        code = nested
                    elif isinstance(nested, dict):
                        code = nested.get("error") or nested.get("code")
                    else:
                        code = payload.get("code")
                else:
                    code = None
            except Exception:
                code = None
            if code in ("authorization_pending", "slow_down"):
                if code == "slow_down":
                    interval += 5
                continue
            raise
    return out({"error": "auth_timeout", "hint": "Run action=auth again"})


def token() -> str:
    cached = cache_read()
    if cached.get("access_token") and int(cached.get("expires_at", 0)) > time.time():
        return cached["access_token"]
    refreshed = refresh_token(cached)
    if refreshed:
        cache_write(refreshed)
        return refreshed["access_token"]
    raise RuntimeError(out({"error": "not_authenticated", "hint": "Run action=auth first"}))


def qs(params: dict[str, Any]) -> str:
    return urllib.parse.urlencode(params)


def quote(value: str) -> str:
    return urllib.parse.quote(value, safe="")


def safe_name(name: str | None) -> str:
    name = name or "file.bin"
    safe = "".join(c if c.isalnum() or c in "._- " else "_" for c in name).strip()
    return safe[:180] or "file.bin"


def mail_summary(message: dict[str, Any]) -> dict[str, Any]:
    sender = ((message.get("from") or {}).get("emailAddress") or {})
    return {
        "id": message.get("id"),
        "received": message.get("receivedDateTime"),
        "from": sender.get("address") or sender.get("name"),
        "subject": message.get("subject"),
        "preview": message.get("bodyPreview"),
        "is_read": message.get("isRead"),
        "has_attachments": message.get("hasAttachments"),
    }


def mail_list(args: dict[str, Any]) -> str:
    limit = max(1, min(int(args.get("limit", 5)), 50))
    url = GRAPH + "/me/messages?" + qs(
        {
            "$top": limit,
            "$orderby": "receivedDateTime desc",
            "$select": "id,receivedDateTime,from,subject,bodyPreview,isRead,hasAttachments",
        }
    )
    return out({"messages": [mail_summary(message) for message in request_json("GET", url, token()).get("value", [])]})


def mail_search(args: dict[str, Any]) -> str:
    query = args.get("query") or args.get("q")
    if not query:
        return out({"error": "missing_query"})
    limit = max(1, min(int(args.get("limit", 10)), 50))
    url = GRAPH + "/me/messages?" + qs(
        {
            "$top": limit,
            "$search": f'"{query}"',
            "$select": "id,receivedDateTime,from,subject,bodyPreview,isRead,hasAttachments",
        }
    )
    return out({"messages": [mail_summary(message) for message in request_json("GET", url, token()).get("value", [])]})


def mail_read(args: dict[str, Any]) -> str:
    message_id = args.get("message_id") or args.get("id")
    if not message_id:
        return out({"error": "missing_message_id"})
    url = f"{GRAPH}/me/messages/{quote(message_id)}?" + qs(
        {"$select": "id,receivedDateTime,from,toRecipients,ccRecipients,subject,body,isRead,hasAttachments"}
    )
    message = request_json("GET", url, token())
    body = message.get("body") or {}
    return out(
        {
            "id": message.get("id"),
            "received": message.get("receivedDateTime"),
            "from": ((message.get("from") or {}).get("emailAddress") or {}),
            "subject": message.get("subject"),
            "body_type": body.get("contentType"),
            "body": body.get("content"),
            "is_read": message.get("isRead"),
            "has_attachments": message.get("hasAttachments"),
        }
    )


def mail_send(args: dict[str, Any]) -> str:
    confirmation = confirm_required(args, "mail_send")
    if confirmation:
        return confirmation
    recipients = args.get("to")
    if isinstance(recipients, str):
        recipients = [recipients]
    if not recipients or not args.get("subject"):
        return out({"error": "missing_to_or_subject"})
    message = {
        "subject": args.get("subject"),
        "body": {"contentType": "Text", "content": args.get("body", "")},
        "toRecipients": [{"emailAddress": {"address": recipient}} for recipient in recipients],
    }
    request_json("POST", GRAPH + "/me/sendMail", token(), data={"message": message, "saveToSentItems": True})
    return out({"ok": True})


def mail_mark_read(args: dict[str, Any]) -> str:
    confirmation = confirm_required(args, "mail_mark_read")
    if confirmation:
        return confirmation
    message_id = args.get("message_id") or args.get("id")
    if not message_id:
        return out({"error": "missing_message_id"})
    request_json("PATCH", f"{GRAPH}/me/messages/{quote(message_id)}", token(), data={"isRead": bool(args.get("is_read", True))})
    return out({"ok": True})


def mail_delete(args: dict[str, Any]) -> str:
    confirmation = confirm_required(args, "mail_delete")
    if confirmation:
        return confirmation
    message_id = args.get("message_id") or args.get("id")
    if not message_id:
        return out({"error": "missing_message_id"})
    request_json("DELETE", f"{GRAPH}/me/messages/{quote(message_id)}", token())
    return out({"ok": True})


def mail_folders(args: dict[str, Any]) -> str:
    data = request_json("GET", GRAPH + "/me/mailFolders?" + qs({"$top": 100}), token())
    return out(
        {
            "folders": [
                {
                    "id": folder.get("id"),
                    "name": folder.get("displayName"),
                    "unread": folder.get("unreadItemCount"),
                    "total": folder.get("totalItemCount"),
                }
                for folder in data.get("value", [])
            ]
        }
    )


def attachments(args: dict[str, Any]) -> str:
    message_id = args.get("message_id") or args.get("id")
    if not message_id:
        return out({"error": "missing_message_id"})
    data = request_json("GET", f"{GRAPH}/me/messages/{quote(message_id)}/attachments", token())
    return out(
        {
            "message_id": message_id,
            "attachments": [
                {
                    "id": attachment.get("id"),
                    "name": attachment.get("name"),
                    "content_type": attachment.get("contentType"),
                    "size": attachment.get("size"),
                    "is_inline": attachment.get("isInline"),
                    "type": attachment.get("@odata.type"),
                }
                for attachment in data.get("value", [])
            ],
        }
    )


def download_attachment(args: dict[str, Any]) -> str:
    message_id = args.get("message_id") or args.get("id")
    attachment_id = args.get("attachment_id")
    if not message_id or not attachment_id:
        return out({"error": "missing_ids"})
    attachment = request_json("GET", f"{GRAPH}/me/messages/{quote(message_id)}/attachments/{quote(attachment_id)}", token())
    content = attachment.get("contentBytes")
    if not content:
        return out({"error": "no_contentBytes", "name": attachment.get("name")})
    ATTACH_DIR.mkdir(parents=True, exist_ok=True)
    path = ATTACH_DIR / safe_name(attachment.get("name"))
    path.write_bytes(base64.b64decode(content))
    return out({"saved_to": str(path), "name": path.name, "size": path.stat().st_size, "content_type": attachment.get("contentType")})


def contacts_list(args: dict[str, Any]) -> str:
    limit = max(1, min(int(args.get("limit", 25)), 100))
    data = request_json(
        "GET",
        GRAPH + "/me/contacts?" + qs({"$top": limit, "$select": "id,displayName,emailAddresses,mobilePhone,businessPhones"}),
        token(),
    )
    return out(
        {
            "contacts": [
                {
                    "id": contact.get("id"),
                    "name": contact.get("displayName"),
                    "emails": contact.get("emailAddresses"),
                    "mobile": contact.get("mobilePhone"),
                    "phones": contact.get("businessPhones"),
                }
                for contact in data.get("value", [])
            ]
        }
    )


def contacts_search(args: dict[str, Any]) -> str:
    query = (args.get("query") or args.get("q") or "").lower()
    if not query:
        return out({"error": "missing_query"})
    contacts = json.loads(contacts_list({"limit": 100}))["contacts"]
    result = [contact for contact in contacts if query in json.dumps(contact, ensure_ascii=False).lower()]
    return out({"contacts": result[: int(args.get("limit", 20))]})


def people_list(args: dict[str, Any]) -> str:
    limit = max(1, min(int(args.get("limit", 25)), 100))
    data = request_json(
        "GET",
        GRAPH
        + "/me/people?"
        + qs(
            {
                "$top": limit,
                "$select": "id,displayName,givenName,surname,scoredEmailAddresses,phones",
            }
        ),
        token(),
    )
    return out(
        {
            "people": [
                {
                    "id": person.get("id"),
                    "name": person.get("displayName"),
                    "given_name": person.get("givenName"),
                    "surname": person.get("surname"),
                    "emails": person.get("scoredEmailAddresses"),
                    "phones": person.get("phones"),
                }
                for person in data.get("value", [])
            ]
        }
    )


def people_search(args: dict[str, Any]) -> str:
    query = (args.get("query") or args.get("q") or "").lower()
    if not query:
        return out({"error": "missing_query"})
    people = json.loads(people_list({"limit": 100}))["people"]
    result = [person for person in people if query in json.dumps(person, ensure_ascii=False).lower()]
    return out({"people": result[: int(args.get("limit", 20))]})


def contacts_create(args: dict[str, Any]) -> str:
    confirmation = confirm_required(args, "contacts_create")
    if confirmation:
        return confirmation
    data = {
        "displayName": args.get("name"),
        "emailAddresses": [{"address": args.get("email"), "name": args.get("name")}] if args.get("email") else [],
    }
    if args.get("mobile"):
        data["mobilePhone"] = args.get("mobile")
    return out(request_json("POST", GRAPH + "/me/contacts", token(), data=data))


def calendar_list(args: dict[str, Any]) -> str:
    limit = max(1, min(int(args.get("limit", 20)), 50))
    data = request_json(
        "GET",
        GRAPH + "/me/events?" + qs({"$top": limit, "$orderby": "start/dateTime", "$select": "id,subject,start,end,location,organizer"}),
        token(),
    )
    return out(
        {
            "events": [
                {
                    "id": event.get("id"),
                    "subject": event.get("subject"),
                    "start": event.get("start"),
                    "end": event.get("end"),
                    "location": event.get("location"),
                }
                for event in data.get("value", [])
            ]
        }
    )


def calendar_create(args: dict[str, Any]) -> str:
    confirmation = confirm_required(args, "calendar_create")
    if confirmation:
        return confirmation
    event = {
        "subject": args.get("subject"),
        "start": {"dateTime": args.get("start"), "timeZone": args.get("timezone", "Europe/Madrid")},
        "end": {"dateTime": args.get("end"), "timeZone": args.get("timezone", "Europe/Madrid")},
    }
    if args.get("location"):
        event["location"] = {"displayName": args.get("location")}
    return out(request_json("POST", GRAPH + "/me/events", token(), data=event))


def files_list(args: dict[str, Any]) -> str:
    limit = max(1, min(int(args.get("limit", 20)), 100))
    data = request_json("GET", GRAPH + "/me/drive/recent?" + qs({"$top": limit}), token())
    return out({"files": [{"id": item.get("id"), "name": item.get("name"), "size": item.get("size"), "web_url": item.get("webUrl")} for item in data.get("value", [])]})


def files_search(args: dict[str, Any]) -> str:
    query = args.get("query") or args.get("q")
    if not query:
        return out({"error": "missing_query"})
    data = request_json("GET", f"{GRAPH}/me/drive/root/search(q='{quote(query)}')", token())
    return out({"files": [{"id": item.get("id"), "name": item.get("name"), "size": item.get("size"), "web_url": item.get("webUrl")} for item in data.get("value", [])]})


def files_download(args: dict[str, Any]) -> str:
    file_id = args.get("file_id") or args.get("id")
    if not file_id:
        return out({"error": "missing_file_id"})
    metadata = request_json("GET", f"{GRAPH}/me/drive/items/{quote(file_id)}", token())
    content = request_json("GET", f"{GRAPH}/me/drive/items/{quote(file_id)}/content", token(), raw=True)
    FILES_DIR.mkdir(parents=True, exist_ok=True)
    path = FILES_DIR / safe_name(metadata.get("name"))
    path.write_bytes(content)
    return out({"saved_to": str(path), "name": path.name, "size": path.stat().st_size})


def handle(args: dict[str, Any]) -> str:
    action = args.get("action")
    aliases = {"list": "mail_list", "read": "mail_list", "latest": "mail_list", "search": "mail_search"}
    action = aliases.get(action, action)
    funcs = {
        "auth": lambda _: auth(),
        "mail_list": mail_list,
        "mail_search": mail_search,
        "mail_read": mail_read,
        "mail_send": mail_send,
        "mail_mark_read": mail_mark_read,
        "mail_delete": mail_delete,
        "mail_folders": mail_folders,
        "attachments": attachments,
        "list_attachments": attachments,
        "download_attachment": download_attachment,
        "contacts_list": contacts_list,
        "contacts_search": contacts_search,
        "contacts_create": contacts_create,
        "people_list": people_list,
        "people_search": people_search,
        "calendar_list": calendar_list,
        "calendar_create": calendar_create,
        "files_list": files_list,
        "files_search": files_search,
        "files_download": files_download,
    }
    if action in funcs:
        return funcs[action](args)
    return out({"error": "unknown_action", "valid_actions": sorted(funcs)})


ACTIONS = [
    "auth",
    "mail_list",
    "mail_search",
    "mail_read",
    "mail_send",
    "mail_mark_read",
    "mail_delete",
    "mail_folders",
    "attachments",
    "list_attachments",
    "download_attachment",
    "contacts_list",
    "contacts_search",
    "contacts_create",
    "people_list",
    "people_search",
    "calendar_list",
    "calendar_create",
    "files_list",
    "files_search",
    "files_download",
    "list",
    "read",
    "latest",
    "search",
]

registry.register(
    name="outlook_email",
    toolset=_TOOLSET,
    schema={
        "name": "outlook_email",
        "description": "Microsoft Graph Outlook, contacts, calendar, and OneDrive helper. Destructive or external actions require confirm:true.",
        "parameters": {
            "type": "object",
            "properties": {
                "action": {"type": "string", "enum": ACTIONS},
                "limit": {"type": "integer"},
                "query": {"type": "string"},
                "q": {"type": "string"},
                "id": {"type": "string"},
                "message_id": {"type": "string"},
                "attachment_id": {"type": "string"},
                "file_id": {"type": "string"},
                "to": {"type": ["string", "array"]},
                "subject": {"type": "string"},
                "body": {"type": "string"},
                "name": {"type": "string"},
                "email": {"type": "string"},
                "mobile": {"type": "string"},
                "start": {"type": "string"},
                "end": {"type": "string"},
                "timezone": {"type": "string"},
                "location": {"type": "string"},
                "confirm": {"type": "boolean"},
            },
            "required": ["action"],
        },
    },
    handler=handle,
    emoji="mail",
)
