"""
Service layer cho CuCu Note với MongoDB backend.
Full features: memos, attachments, relations, reactions, embeddings, inbox, settings...
"""

from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from typing import Any, List

from bson import ObjectId
import numpy as np

from . import schemas
from .mongodb import mongodb_client, generate_uid, utc_now, serialize_doc
from langchain_openai import OpenAIEmbeddings


class InstanceService:
    async def get_instance_info(self) -> schemas.InstanceInfo:
        return schemas.InstanceInfo(mode="dev", version="0.0.1-mongodb")

    async def update_instance_info(self, payload: schemas.InstanceUpdate) -> schemas.InstanceInfo:
        return schemas.InstanceInfo(mode="dev", version="0.0.1-mongodb")

    async def get_instance_setting(self) -> schemas.InstanceSetting:
        return schemas.InstanceSetting()


class AuthService:
    async def sign_in(self, payload: schemas.AuthSignInRequest) -> schemas.AuthSignInResponse:
        return schemas.AuthSignInResponse(accessToken="stub-token")

    async def sign_up(self, payload: schemas.AuthSignUpRequest) -> schemas.AuthSignUpResponse:
        return schemas.AuthSignUpResponse(user_id="1")

    async def get_me(self, token: str | None = None) -> schemas.AuthMeResponse:
        if not token:
            raise ValueError("Missing authentication token")
        return schemas.AuthMeResponse(id="1", email="demo@example.com")


class UserService:
    async def list_users(self) -> List[schemas.UserResponse]:
        return []

    async def create_user(self, payload: schemas.UserCreate) -> schemas.UserResponse:
        email = payload.email or "demo@example.com"
        return schemas.UserResponse(id="1", email=email, nickname=None)

    async def get_user(self, user_id: str) -> schemas.UserResponse:
        return schemas.UserResponse(id=user_id, email="demo@example.com", nickname=None)

    async def update_user(self, user_id: str, payload: schemas.UserUpdate) -> schemas.UserResponse:
        return schemas.UserResponse(id=user_id, email=payload.email or "demo@example.com", nickname=payload.nickname)


class MemoService:
    """Full-featured Memo service with MongoDB backend."""

    async def list_memos(
        self,
        user_id: str | None = None,
        creator_id: str | None = None,
        tag: str | None = None,
        visibility: str | None = None,
        pinned: bool | None = None,
        row_status: str | None = None,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
    ) -> List[schemas.MemoResponse]:
        """
        List memos with filtering.
        """
        query: dict[str, Any] = {}

        if creator_id:
            # Specific creator requested (e.g. profile view)
            query["creator_id"] = creator_id

            # Access control for that creator's memos
            if user_id and creator_id == user_id:
                # Viewing own profile: show all
                pass
            else:
                # Viewing others: show PUBLIC and PROTECTED (if logged in)
                if user_id and user_id != "anonymous":
                    query["visibility"] = {"$in": ["PUBLIC", "PROTECTED"]}
                else:
                    query["visibility"] = "PUBLIC"

        elif user_id and user_id != "anonymous":
            # Feed view (no specific creator): user's own memos + PUBLIC + PROTECTED memos
            query["$or"] = [
                {"creator_id": user_id},
                {"visibility": "PUBLIC"},
                {"visibility": "PROTECTED"},
            ]
        else:
            # Guest feed: only show public memos
            query["visibility"] = "PUBLIC"

        # Apply filters
        if visibility:
            query["visibility"] = visibility
        if pinned is not None:
            query["pinned"] = pinned
        if row_status:
            query["row_status"] = row_status
        else:
            # Default: only show NORMAL (not archived)
            query["row_status"] = {"$ne": "ARCHIVED"}

        # Exclude comments by default (only show parent memos)
        query["parent"] = {"$exists": False}

        # Tag filter
        if tag:
            query["payload.tags"] = tag

        # Date Range filter
        date_filter: dict[str, Any] = {}
        if start_date:
            date_filter["$gte"] = start_date
        if end_date:
            date_filter["$lte"] = end_date

        if date_filter:
            query["created_at"] = date_filter

        cursor = mongodb_client.memos.find(query).sort("created_at", -1)
        docs = await cursor.to_list(length=100)

        memos: list[schemas.MemoResponse] = []
        for doc in docs:
            memo_response = self._doc_to_response(doc)
            doc_visibility = doc.get("visibility", "PRIVATE")
            memo_response.comment_count = await self._get_comment_count(str(doc["_id"]), doc_visibility, user_id)
            memos.append(memo_response)
        return memos

    async def create_memo(
        self,
        payload: schemas.MemoCreate,
        user_id: str | None = None,
        parent_id: str | None = None,
        anonymous_id: str | None = None,
    ) -> schemas.MemoResponse:
        """Create a memo for the specified user."""
        creator = user_id or "anonymous"
        now = payload.create_time or utc_now()
        if now.tzinfo is None:
            now = now.replace(tzinfo=timezone.utc)

        memo_payload = {
            "tags": payload.tags or [],
            "hasLink": False,
            "hasTaskList": False,
            "hasCode": False,
            "hasIncompleteTasks": False,
        }

        doc: dict[str, Any] = {
            "uid": generate_uid(),
            "creator_id": creator,
            "content": payload.content,
            "visibility": payload.visibility or "PRIVATE",
            "pinned": False,
            "payload": memo_payload,
            "row_status": "NORMAL",
            "created_at": now,
            "updated_at": now,
        }

        if parent_id:
            doc["parent"] = parent_id

        if anonymous_id:
            doc["anonymous_id"] = anonymous_id

        result = await mongodb_client.memos.insert_one(doc)
        doc["_id"] = result.inserted_id

        await self._create_embedding(str(result.inserted_id), payload.content, payload.tags or [])

        return self._doc_to_response(doc)

    async def get_memo(
        self,
        memo_id: str,
        user_id: str | None = None,
    ) -> schemas.MemoResponse:
        """Get a memo by ID. Access control: owner or public."""
        doc = None
        if ObjectId.is_valid(memo_id):
            doc = await mongodb_client.memos.find_one({"_id": ObjectId(memo_id)})
        if not doc:
            doc = await mongodb_client.memos.find_one({"uid": memo_id})

        if not doc:
            raise ValueError(f"Memo {memo_id} not found")

        memo_creator = doc.get("creator_id", "anonymous")
        memo_visibility = doc.get("visibility", "PRIVATE")

        # Owner can always access
        if memo_creator == user_id:
            response = self._doc_to_response(doc)
            response.comment_count = await self._get_comment_count(str(doc["_id"]), memo_visibility, user_id)
            return response

        # PUBLIC: anyone can access (including anonymous)
        if memo_visibility == "PUBLIC":
            response = self._doc_to_response(doc)
            response.comment_count = await self._get_comment_count(str(doc["_id"]), memo_visibility, user_id)
            return response

        # PROTECTED: only logged-in users can access
        if memo_visibility == "PROTECTED":
            if user_id and user_id != "anonymous":
                response = self._doc_to_response(doc)
                response.comment_count = await self._get_comment_count(str(doc["_id"]), memo_visibility, user_id)
                return response
            raise ValueError("Access denied: PROTECTED memo requires authentication")

        # PRIVATE: only owner can access (handled above)
        raise ValueError(f"Access denied to memo {memo_id}")

    async def _get_comment_count(self, memo_id: str, memo_visibility: str, user_id: str | None = None) -> int:
        """Get comment count for a memo based on visibility and user access."""
        query: dict[str, Any] = {
            "parent": {"$in": [memo_id]},
            "row_status": {"$ne": "ARCHIVED"},
        }

        if memo_visibility == "PUBLIC":
            query["visibility"] = "PUBLIC"
        elif memo_visibility == "PROTECTED" and user_id:
            query["$or"] = [
                {"visibility": "PUBLIC"},
                {"visibility": "PROTECTED"},
            ]
        elif memo_visibility == "PRIVATE":
            if user_id:
                # Owner can see all comments
                pass
            else:
                return 0

        count = await mongodb_client.memos.count_documents(query)
        return count

    async def update_memo(
        self,
        memo_id: str,
        payload: schemas.MemoUpdate,
        user_id: str | None = None,
    ) -> schemas.MemoResponse:
        """Update a memo. Only the owner can update."""
        doc = None
        if ObjectId.is_valid(memo_id):
            doc = await mongodb_client.memos.find_one({"_id": ObjectId(memo_id)})
        if not doc:
            doc = await mongodb_client.memos.find_one({"uid": memo_id})

        if not doc:
            raise ValueError(f"Memo {memo_id} not found")

        memo_creator = doc.get("creator_id", "anonymous")
        if memo_creator != user_id:
            raise ValueError(f"Access denied: you don't own memo {memo_id}")

        update_fields: dict[str, Any] = {"updated_at": utc_now()}

        if payload.content is not None:
            update_fields["content"] = payload.content
        if payload.visibility is not None:
            update_fields["visibility"] = payload.visibility
        if payload.tags is not None:
            update_fields["payload.tags"] = payload.tags
        if payload.pinned is not None:
            update_fields["pinned"] = payload.pinned
        if payload.row_status is not None:
            update_fields["row_status"] = payload.row_status

        await mongodb_client.memos.update_one(
            {"_id": doc["_id"]},
            {"$set": update_fields},
        )

        if payload.content is not None:
            await self._create_embedding(
                str(doc["_id"]),
                payload.content,
                payload.tags or doc.get("payload", {}).get("tags", []),
            )

        return await self.get_memo(str(doc["_id"]), user_id=user_id)

    async def delete_memo(self, memo_id: str, user_id: str | None = None) -> None:
        """Delete a memo. Only the owner can delete."""
        doc = None
        if ObjectId.is_valid(memo_id):
            doc = await mongodb_client.memos.find_one({"_id": ObjectId(memo_id)})
        if not doc:
            doc = await mongodb_client.memos.find_one({"uid": memo_id})

        if not doc:
            raise ValueError(f"Memo {memo_id} not found")

        memo_creator = doc.get("creator_id", "anonymous")
        if memo_creator != user_id:
            raise ValueError(f"Access denied: you don't own memo {memo_id}")

        await mongodb_client.memos.delete_one({"_id": doc["_id"]})
        await mongodb_client.memo_embeddings.delete_many({"memo_id": str(doc["_id"])})
        await mongodb_client.attachments.delete_many({"memo_id": str(doc["_id"])})
        await mongodb_client.memo_relations.delete_many(
            {
                "$or": [
                    {"memo_id": str(doc["_id"])},
                    {"related_memo_id": str(doc["_id"])},
                ]
            }
        )
        await mongodb_client.reactions.delete_many({"content_id": str(doc["_id"])})

    async def pin_memo(self, memo_id: str, pinned: bool, user_id: str | None = None) -> schemas.MemoResponse:
        """Pin or unpin a memo."""
        return await self.update_memo(
            memo_id,
            schemas.MemoUpdate(pinned=pinned),
            user_id=user_id,
        )

    async def archive_memo(self, memo_id: str, user_id: str | None = None) -> schemas.MemoResponse:
        """Archive a memo."""
        return await self.update_memo(
            memo_id,
            schemas.MemoUpdate(row_status="ARCHIVED"),
            user_id=user_id,
        )

    async def restore_memo(self, memo_id: str, user_id: str | None = None) -> schemas.MemoResponse:
        """Restore an archived memo."""
        return await self.update_memo(
            memo_id,
            schemas.MemoUpdate(row_status="NORMAL"),
            user_id=user_id,
        )

    def _doc_to_response(self, doc: dict) -> schemas.MemoResponse:
        """Convert MongoDB document to MemoResponse."""
        payload = doc.get("payload", {})
        created_at = doc.get("created_at")
        updated_at = doc.get("updated_at")

        if isinstance(created_at, datetime):
            if created_at.tzinfo:
                created_at = created_at.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
            else:
                created_at = created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
        if isinstance(updated_at, datetime):
            if updated_at.tzinfo:
                updated_at = updated_at.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
            else:
                updated_at = updated_at.strftime("%Y-%m-%dT%H:%M:%SZ")

        return schemas.MemoResponse(
            id=str(doc["_id"]),
            uid=doc.get("uid", ""),
            content=doc.get("content", ""),
            visibility=doc.get("visibility", "PRIVATE"),
            tags=payload.get("tags", []),
            pinned=doc.get("pinned", False),
            row_status=doc.get("row_status", "NORMAL"),
            creator_id=doc.get("creator_id", "anonymous"),
            create_time=created_at,
            update_time=updated_at,
            display_time=created_at,
            parent=doc.get("parent"),
        )

    async def _create_embedding(self, memo_id: str, content: str, tags: list[str]) -> None:
        """Create or update embedding for a memo."""
        try:
            api_key = os.getenv("OPENAI_API_KEY")
            if not api_key:
                return

            embedder = OpenAIEmbeddings(model="text-embedding-3-small", api_key=api_key)
            vec = embedder.embed_query(content)

            if not vec:
                return

            now = utc_now()
            date_key = now.strftime("%Y-%m-%d")

            await mongodb_client.memo_embeddings.update_one(
                {"memo_id": memo_id},
                {
                    "$set": {
                        "memo_id": memo_id,
                        "content": content,
                        "tags": tags,
                        "date_key": date_key,
                        "embedding": vec,
                        "dim": len(vec),
                        "model": "text-embedding-3-small",
                        "updated_at": now,
                    },
                    "$setOnInsert": {
                        "created_at": now,
                    },
                },
                upsert=True,
            )
        except Exception as e:
            import logging

            logging.warning(f"Failed to create embedding: {e}")


class AttachmentService:
    """Attachment service with MongoDB backend."""

    async def list_attachments(self, memo_id: str | None = None) -> List[schemas.AttachmentResponse]:
        query: dict[str, Any] = {}
        if memo_id:
            query["memo_id"] = memo_id

        cursor = mongodb_client.attachments.find(query)
        docs = await cursor.to_list(length=100)

        return [self._doc_to_response(doc) for doc in docs]

    async def upload_attachment(self, file, memo_id: str | None = None, user_id: str | None = None) -> schemas.AttachmentResponse:
        now = utc_now()

        doc = {
            "uid": generate_uid(),
            "creator_id": user_id or "anonymous",
            "filename": getattr(file, "filename", "upload.bin"),
            "type": getattr(file, "content_type", "application/octet-stream"),
            "size": 0,
            "storage_type": "LOCAL",
            "reference": "",
            "memo_id": memo_id,
            "created_at": now,
            "updated_at": now,
        }

        result = await mongodb_client.attachments.insert_one(doc)
        doc["_id"] = result.inserted_id

        return self._doc_to_response(doc)

    async def get_attachment(self, attachment_id: str) -> schemas.AttachmentResponse:
        doc = None
        if ObjectId.is_valid(attachment_id):
            doc = await mongodb_client.attachments.find_one({"_id": ObjectId(attachment_id)})
        if not doc:
            doc = await mongodb_client.attachments.find_one({"uid": attachment_id})

        if not doc:
            raise ValueError(f"Attachment {attachment_id} not found")

        return self._doc_to_response(doc)

    async def delete_attachment(self, attachment_id: str, user_id: str | None = None) -> None:
        doc = None
        if ObjectId.is_valid(attachment_id):
            doc = await mongodb_client.attachments.find_one({"_id": ObjectId(attachment_id)})
        if not doc:
            doc = await mongodb_client.attachments.find_one({"uid": attachment_id})

        if not doc:
            raise ValueError(f"Attachment {attachment_id} not found")

        if doc.get("creator_id") != user_id:
            raise ValueError("Access denied")

        await mongodb_client.attachments.delete_one({"_id": doc["_id"]})

    def _doc_to_response(self, doc: dict) -> schemas.AttachmentResponse:
        return schemas.AttachmentResponse(
            id=str(doc["_id"]),
            uid=doc.get("uid", ""),
            filename=doc.get("filename", ""),
            url=doc.get("reference", ""),
            type=doc.get("type", ""),
            size=doc.get("size", 0),
        )


class MemoRelationService:
    """Memo relations service (comments, references)."""

    async def create_relation(
        self,
        memo_id: str,
        related_memo_id: str,
        relation_type: str = "REFERENCE",
    ) -> dict:
        now = utc_now()

        doc = {
            "memo_id": memo_id,
            "related_memo_id": related_memo_id,
            "type": relation_type,
            "created_at": now,
        }

        await mongodb_client.memo_relations.insert_one(doc)
        return serialize_doc(doc)

    async def list_relations(self, memo_id: str, related_memo_id: str | None = None, relation_type: str | None = None) -> list[dict]:
        """List memo relations."""
        query: dict[str, Any] = {
            "$or": [
                {"memo_id": memo_id},
                {"related_memo_id": memo_id},
            ]
        }

        if related_memo_id:
            query["related_memo_id"] = related_memo_id
        if relation_type:
            query["type"] = relation_type

        cursor = mongodb_client.memo_relations.find(query)
        docs = await cursor.to_list(length=100)
        return [serialize_doc(doc) for doc in docs]

    async def delete_relation(self, memo_id: str, related_memo_id: str) -> None:
        await mongodb_client.memo_relations.delete_one(
            {
                "memo_id": memo_id,
                "related_memo_id": related_memo_id,
            }
        )


class ReactionService:
    """Reactions service (emoji reactions)."""

    async def add_reaction(
        self,
        content_id: str,
        reaction_type: str,
        user_id: str | None = None,
    ) -> dict:
        now = utc_now()

        doc = {
            "content_id": content_id,
            "creator_id": user_id or "anonymous",
            "reaction_type": reaction_type,
            "created_at": now,
        }

        await mongodb_client.reactions.update_one(
            {
                "content_id": content_id,
                "creator_id": user_id or "anonymous",
                "reaction_type": reaction_type,
            },
            {"$set": doc},
            upsert=True,
        )

        return serialize_doc(doc)

    async def list_reactions(self, content_id: str) -> list[dict]:
        cursor = mongodb_client.reactions.find({"content_id": content_id})
        docs = await cursor.to_list(length=100)
        return [serialize_doc(doc) for doc in docs]

    async def remove_reaction(
        self,
        content_id: str,
        reaction_type: str,
        user_id: str | None = None,
    ) -> None:
        await mongodb_client.reactions.delete_one(
            {
                "content_id": content_id,
                "creator_id": user_id or "anonymous",
                "reaction_type": reaction_type,
            }
        )


class ShortcutService:
    async def list_shortcuts(self) -> List[schemas.ShortcutResponse]:
        return []

    async def create_shortcut(self, payload: schemas.ShortcutCreate) -> schemas.ShortcutResponse:
        return schemas.ShortcutResponse(id=1, name=payload.name, filter=payload.filter)

    async def update_shortcut(self, shortcut_id: int, payload: schemas.ShortcutUpdate) -> schemas.ShortcutResponse:
        return schemas.ShortcutResponse(
            id=shortcut_id,
            name=payload.name or "demo",
            filter=payload.filter or "",
        )

    async def delete_shortcut(self, shortcut_id: int) -> None:
        return None


class ActivityService:
    async def list_activities(self) -> List[schemas.ActivityResponse]:
        return []


class IdentityProviderService:
    async def list_identity_providers(self) -> List[schemas.IdentityProviderResponse]:
        return []

    async def create_identity_provider(
        self,
        payload: schemas.IdentityProviderCreate,
    ) -> schemas.IdentityProviderResponse:
        return schemas.IdentityProviderResponse(id=1, name=payload.name, type=payload.type)

    async def update_identity_provider(
        self,
        idp_id: int,
        payload: schemas.IdentityProviderUpdate,
    ) -> schemas.IdentityProviderResponse:
        return schemas.IdentityProviderResponse(id=idp_id, name=payload.name or "demo", type="oidc")


class MemoEmbeddingService:
    """Embedding service for semantic search."""

    async def search_embeddings(
        self,
        query_embedding: list[float],
        top_k: int = 5,
        user_id: str | None = None,
    ) -> list[dict]:
        """Search memos by embedding similarity."""
        cursor = mongodb_client.memo_embeddings.find({})
        docs = await cursor.to_list(length=1000)

        if not docs:
            return []

        q = np.array(query_embedding, dtype=float)
        results: list[dict[str, Any]] = []

        for doc in docs:
            emb = doc.get("embedding", [])
            if not emb:
                continue

            v = np.array(emb, dtype=float)
            if v.shape != q.shape:
                continue

            denom = np.linalg.norm(q) * np.linalg.norm(v)
            sim = float(np.dot(q, v) / denom) if denom != 0 else 0.0

            results.append(
                {
                    "memo_id": doc.get("memo_id"),
                    "content": doc.get("content", ""),
                    "tags": doc.get("tags", []),
                    "score": sim,
                }
            )

        results.sort(key=lambda r: r["score"], reverse=True)
        return results[:top_k]


class InboxService:
    """Inbox notifications service - notifies users of comments/mentions."""

    async def create_notification(
        self,
        sender_id: str,
        receiver_id: str,
        message_type: str,
        memo_id: str | None = None,
        content: str | None = None,
    ) -> dict:
        now = utc_now()

        doc = {
            "sender_id": sender_id,
            "receiver_id": receiver_id,
            "status": "UNREAD",
            "message": {
                "type": message_type,
                "memo_id": memo_id,
                "content": content,
            },
            "created_at": now,
        }

        result = await mongodb_client.inbox.insert_one(doc)
        doc["_id"] = result.inserted_id
        return serialize_doc(doc)

    async def list_notifications(
        self,
        receiver_id: str,
        status: str | None = None,
    ) -> list[dict]:
        query: dict[str, Any] = {"receiver_id": receiver_id}
        if status:
            query["status"] = status

        cursor = mongodb_client.inbox.find(query).sort("created_at", -1)
        docs = await cursor.to_list(length=50)
        return [serialize_doc(doc) for doc in docs]

    async def mark_as_read(self, notification_id: str, receiver_id: str) -> dict | None:
        from bson import ObjectId

        result = await mongodb_client.inbox.find_one_and_update(
            {"_id": ObjectId(notification_id), "receiver_id": receiver_id},
            {"$set": {"status": "READ"}},
            return_document=True,
        )
        return serialize_doc(result) if result else None

    async def mark_all_as_read(self, receiver_id: str) -> int:
        result = await mongodb_client.inbox.update_many(
            {"receiver_id": receiver_id, "status": "UNREAD"},
            {"$set": {"status": "READ"}},
        )
        return result.modified_count

    async def delete_notification(self, notification_id: str, receiver_id: str) -> bool:
        from bson import ObjectId

        result = await mongodb_client.inbox.delete_one(
            {
                "_id": ObjectId(notification_id),
                "receiver_id": receiver_id,
            }
        )
        return result.deleted_count > 0

    async def get_unread_count(self, receiver_id: str) -> int:
        return await mongodb_client.inbox.count_documents(
            {
                "receiver_id": receiver_id,
                "status": "UNREAD",
            }
        )


class UserSettingsService:
    """User settings/preferences service."""

    async def get_settings(self, user_id: str) -> dict:
        doc = await mongodb_client.user_settings.find_one({"user_id": user_id})

        if not doc:
            default = {
                "user_id": user_id,
                "locale": "en",
                "theme": "system",
                "memo_visibility": "PRIVATE",
                "enable_notifications": True,
                "created_at": utc_now(),
                "updated_at": utc_now(),
            }
            await mongodb_client.user_settings.insert_one(default)
            return serialize_doc(default)

        return serialize_doc(doc)

    async def update_settings(self, user_id: str, updates: dict) -> dict:
        allowed_fields = {
            "locale",
            "theme",
            "memo_visibility",
            "enable_notifications",
            "timezone",
        }
        filtered_updates = {k: v for k, v in updates.items() if k in allowed_fields}
        filtered_updates["updated_at"] = utc_now()

        result = await mongodb_client.user_settings.find_one_and_update(
            {"user_id": user_id},
            {"$set": filtered_updates},
            upsert=True,
            return_document=True,
        )
        return serialize_doc(result)


class S3StorageService:
    """S3 storage service for attachments."""

    def __init__(self) -> None:
        self.bucket_name = os.getenv("S3_BUCKET_NAME", "cucu-note-attachments")
        self.region = os.getenv("AWS_REGION", "ap-southeast-1")
        self._client = None

    def _get_client(self):
        if self._client is None:
            try:
                import boto3

                self._client = boto3.client(
                    "s3",
                    region_name=self.region,
                    aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
                    aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
                )
            except ImportError:
                raise RuntimeError("boto3 not installed. Run: pip install boto3")
        return self._client

    async def upload_file(
        self,
        file_content: bytes,
        filename: str,
        content_type: str = "application/octet-stream",
        user_id: str = "anonymous",
    ) -> dict:
        import asyncio

        timestamp = datetime.utcnow().strftime("%Y/%m/%d")
        key = f"{user_id}/{timestamp}/{generate_uid()}_{filename}"

        loop = asyncio.get_event_loop()
        await loop.run_in_executor(
            None,
            lambda: self._get_client().put_object(
                Bucket=self.bucket_name,
                Key=key,
                Body=file_content,
                ContentType=content_type,
            ),
        )

        url = f"https://{self.bucket_name}.s3.{self.region}.amazonaws.com/{key}"

        return {
            "key": key,
            "url": url,
            "bucket": self.bucket_name,
            "size": len(file_content),
        }

    async def delete_file(self, key: str) -> bool:
        import asyncio

        try:
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(
                None,
                lambda: self._get_client().delete_object(
                    Bucket=self.bucket_name,
                    Key=key,
                ),
            )
            return True
        except Exception:
            return False

    async def get_presigned_url(self, key: str, expires_in: int = 3600) -> str:
        import asyncio

        loop = asyncio.get_event_loop()
        url = await loop.run_in_executor(
            None,
            lambda: self._get_client().generate_presigned_url(
                "get_object",
                Params={"Bucket": self.bucket_name, "Key": key},
                ExpiresIn=expires_in,
            ),
        )
        return url


# =========================================================================
# Dependency providers
# =========================================================================


def get_instance_service() -> InstanceService:
    return InstanceService()


def get_auth_service() -> AuthService:
    return AuthService()


def get_user_service() -> UserService:
    return UserService()


def get_memo_service() -> MemoService:
    return MemoService()


def get_attachment_service() -> AttachmentService:
    return AttachmentService()


def get_shortcut_service() -> ShortcutService:
    return ShortcutService()


def get_activity_service() -> ActivityService:
    return ActivityService()


def get_idp_service() -> IdentityProviderService:
    return IdentityProviderService()


def get_memo_relation_service() -> MemoRelationService:
    return MemoRelationService()


def get_reaction_service() -> ReactionService:
    return ReactionService()


def get_memo_embedding_service() -> MemoEmbeddingService:
    return MemoEmbeddingService()


def get_inbox_service() -> InboxService:
    return InboxService()


def get_user_settings_service() -> UserSettingsService:
    return UserSettingsService()


def get_s3_storage_service() -> S3StorageService:
    return S3StorageService()


