"""
Service layer cho Memos-style backend với SQLite nhẹ.
"""

from __future__ import annotations

import json
import os
from datetime import datetime
from typing import List

from fastapi import Depends
import numpy as np

from . import schemas
from .db import execute, fetch_all, fetch_one, init_memo_db
from langchain_openai import OpenAIEmbeddings

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

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

    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) -> schemas.AuthMeResponse:
        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: int) -> schemas.UserResponse:
        return schemas.UserResponse(id=user_id, email="demo@example.com", nickname=None)

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


class MemoService:
    async def list_memos(self, user_id: str | None = None, tag: str | None = None) -> List[schemas.MemoResponse]:
        """
        List memos for a specific user.
        - If user_id is provided: show user's memos + public memos
        - If user_id is None (guest): show only public memos
        """
        await init_memo_db()
        
        if user_id:
            # Show user's own memos + public memos from others
            rows = await fetch_all(
                """
                SELECT id, content, visibility, tags_json, creator_id, created_at, updated_at
                FROM memos 
                WHERE creator_id = ? OR visibility = 'PUBLIC'
                ORDER BY created_at DESC
                """,
                (user_id,)
            )
        else:
            # Guest: only show public memos
            rows = await fetch_all(
                """
                SELECT id, content, visibility, tags_json, creator_id, created_at, updated_at
                FROM memos 
                WHERE visibility = 'PUBLIC'
                ORDER BY created_at DESC
                """
            )

        memos: list[schemas.MemoResponse] = []
        for row in rows:
            tags = _decode_tags(row.get("tags_json"))
            if tag and tag not in tags:
                continue
            memos.append(
                schemas.MemoResponse(
                    id=row["id"],
                    content=row["content"],
                    visibility=row.get("visibility") or "PRIVATE",
                    tags=tags,
                    creator_id=row.get("creator_id") or "anonymous",
                    create_time=row.get("created_at"),
                    update_time=row.get("updated_at"),
                    display_time=row.get("created_at"),
                )
            )
        return memos

    async def create_memo(self, payload: schemas.MemoCreate, user_id: str | None = None) -> schemas.MemoResponse:
        """Create a memo for the specified user."""
        await init_memo_db()
        
        # Use actual user_id from Clerk, or 'anonymous' for guests
        creator = user_id or "anonymous"
        
        tags_json = _encode_tags(payload.tags)
        memo_id = await execute(
            """
            INSERT INTO memos (content, visibility, tags_json, creator_id)
            VALUES (?, ?, ?, ?)
            """,
            (
                payload.content,
                payload.visibility or "PRIVATE",
                tags_json,
                creator,
            ),
        )
        await self._upsert_stub_embedding(
            memo_id=memo_id,
            content=payload.content,
            tags=payload.tags or [],
        )
        return await self.get_memo(memo_id, user_id=creator)

    async def get_memo(self, memo_id: int, user_id: str | None = None) -> schemas.MemoResponse:
        """Get a memo by ID. Access control: owner or public."""
        await init_memo_db()
        row = await fetch_one(
            "SELECT id, content, visibility, tags_json, creator_id, created_at, updated_at FROM memos WHERE id = ?",
            (memo_id,),
        )
        if not row:
            raise ValueError(f"Memo {memo_id} not found")
        
        # Access control: owner can see all, others can only see public
        memo_creator = row.get("creator_id") or "anonymous"
        memo_visibility = row.get("visibility") or "PRIVATE"
        
        if memo_creator != user_id and memo_visibility != "PUBLIC":
            raise ValueError(f"Access denied to memo {memo_id}")
        
        return schemas.MemoResponse(
            id=row["id"],
            content=row["content"],
            visibility=memo_visibility,
            tags=_decode_tags(row.get("tags_json")),
            creator_id=memo_creator,
            create_time=row.get("created_at"),
            update_time=row.get("updated_at"),
            display_time=row.get("created_at"),
        )

    async def update_memo(self, memo_id: int, payload: schemas.MemoUpdate, user_id: str | None = None) -> schemas.MemoResponse:
        """Update a memo. Only the owner can update."""
        await init_memo_db()
        
        # Check ownership first
        row = await fetch_one(
            "SELECT creator_id FROM memos WHERE id = ?",
            (memo_id,),
        )
        if not row:
            raise ValueError(f"Memo {memo_id} not found")
        
        memo_creator = row.get("creator_id") or "anonymous"
        if memo_creator != user_id:
            raise ValueError(f"Access denied: you don't own memo {memo_id}")
        
        current = await self.get_memo(memo_id, user_id=user_id)

        new_content = payload.content if payload.content is not None else current.content
        new_visibility = payload.visibility if payload.visibility is not None else current.visibility
        new_tags = payload.tags if payload.tags is not None else current.tags

        await execute(
            """
            UPDATE memos
            SET content = ?, visibility = ?, tags_json = ?, updated_at = DATETIME('now')
            WHERE id = ? AND creator_id = ?
            """,
            (new_content, new_visibility, _encode_tags(new_tags), memo_id, user_id),
        )
        await self._upsert_stub_embedding(
            memo_id=memo_id,
            content=new_content,
            tags=new_tags or [],
        )

        return await self.get_memo(memo_id, user_id=user_id)

    async def delete_memo(self, memo_id: int, user_id: str | None = None) -> None:
        """Delete a memo. Only the owner can delete."""
        await init_memo_db()
        
        # Check ownership first
        row = await fetch_one(
            "SELECT creator_id FROM memos WHERE id = ?",
            (memo_id,),
        )
        if not row:
            raise ValueError(f"Memo {memo_id} not found")
        
        memo_creator = row.get("creator_id") or "anonymous"
        if memo_creator != user_id:
            raise ValueError(f"Access denied: you don't own memo {memo_id}")
        
        await execute("DELETE FROM memos WHERE id = ? AND creator_id = ?", (memo_id, user_id))
        await execute("DELETE FROM memo_embeddings WHERE memo_id = ?", (memo_id,))
        return None

    async def _upsert_stub_embedding(self, memo_id: int, content: str, tags: list[str]):
        """
        Auto-sync embedding cho memo để tìm kiếm theo ngày/tag.
        Bắt buộc dùng OpenAI embedding; nếu thiếu key hoặc lỗi thì raise.
        """
        await init_memo_db()
        date_key = datetime.utcnow().strftime("%Y-%m-%d")
        tags_json = _encode_tags(tags)

        api_key = os.getenv("OPENAI_API_KEY")
        if not api_key:
            raise RuntimeError("OPENAI_API_KEY is required for memo embeddings")

        embedder = OpenAIEmbeddings(model="text-embedding-3-small", api_key=api_key)
        vec = embedder.embed_query(content)
        if not vec or not isinstance(vec, list):
            raise RuntimeError("Failed to generate embedding for memo")

        emb = vec
        emb_model = "text-embedding-3-small"

        emb_json = json.dumps(emb, ensure_ascii=False)

        await execute("DELETE FROM memo_embeddings WHERE memo_id = ?", (memo_id,))
        await execute(
            """
            INSERT INTO memo_embeddings (memo_id, content, tags_json, date_key, embedding_json, dim, model)
            VALUES (?, ?, ?, ?, ?, ?, ?)
            """,
            (
                memo_id,
                content,
                tags_json,
                date_key,
                emb_json,
                len(emb),
                emb_model,
            ),
        )


class AttachmentService:
    async def list_attachments(self) -> List[schemas.AttachmentResponse]:
        return []

    async def upload_attachment(self, file) -> schemas.AttachmentResponse:
        return schemas.AttachmentResponse(id=1, filename=file.filename, url="/file/demo")

    async def get_attachment(self, attachment_id: int) -> schemas.AttachmentResponse:
        return schemas.AttachmentResponse(id=attachment_id, filename="demo.png", url="/file/demo.png")


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")


# 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()


class MemoEmbeddingService:
    async def create_embedding(self, payload: schemas.MemoEmbeddingCreate) -> schemas.MemoEmbeddingResponse:
        await init_memo_db()
        emb = payload.embedding or []
        if not emb:
            raise ValueError("embedding is required")
        dim = len(emb)
        emb_json = json.dumps(emb, ensure_ascii=False)
        tags_json = _encode_tags(payload.tags)
        await execute(
            """
            INSERT INTO memo_embeddings (memo_id, content, tags_json, date_key, embedding_json, dim, model)
            VALUES (?, ?, ?, ?, ?, ?, ?)
            """,
            (
                payload.memoId,
                payload.content,
                tags_json,
                payload.dateKey,
                emb_json,
                dim,
                payload.model or "stub-embedding",
            ),
        )
        return schemas.MemoEmbeddingResponse(
            id=0,  # SQLite autoinc not returned here
            memoId=payload.memoId,
            content=payload.content,
            tags=payload.tags or [],
            dateKey=payload.dateKey,
            dim=dim,
            model=payload.model or "stub-embedding",
        )

    async def search_embeddings(
        self,
        query_embedding: list[float] | None,
        top_k: int = 5,
        date_key: str | None = None,
        memo_id: int | None = None,
    ) -> list[schemas.MemoEmbeddingResponse]:
        await init_memo_db()
        if not query_embedding:
            raise ValueError("queryEmbedding is required")

        rows = await fetch_all(
            "SELECT id, memo_id, content, tags_json, date_key, embedding_json, dim, model FROM memo_embeddings",
        )
        if date_key:
            rows = [r for r in rows if (r.get("date_key") == date_key)]
        if memo_id:
            rows = [r for r in rows if (r.get("memo_id") == memo_id)]

        q = np.array(query_embedding, dtype=float)
        results: list[schemas.MemoEmbeddingResponse] = []
        for row in rows:
            emb_list = _decode_embedding(row.get("embedding_json"))
            if not emb_list:
                continue
            v = np.array(emb_list, dtype=float)
            if v.shape != q.shape:
                continue
            # cosine similarity
            denom = np.linalg.norm(q) * np.linalg.norm(v)
            sim = float(np.dot(q, v) / denom) if denom != 0 else 0.0
            results.append(
                schemas.MemoEmbeddingResponse(
                    id=row["id"],
                    memoId=row["memo_id"],
                    content=row.get("content") or "",
                    tags=_decode_tags(row.get("tags_json")),
                    dateKey=row.get("date_key"),
                    dim=row.get("dim") or len(emb_list),
                    model=row.get("model") or "",
                    score=sim,
                )
            )

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


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


def _encode_tags(tags: list[str] | None) -> str:
    return json.dumps(tags or [], ensure_ascii=False)


def _decode_tags(raw: str | None) -> list[str]:
    if not raw:
        return []
    try:
        parsed = json.loads(raw)
        return parsed if isinstance(parsed, list) else []
    except json.JSONDecodeError:
        return []


def _decode_embedding(raw: str | None) -> list[float]:
    if not raw:
        return []
    try:
        parsed = json.loads(raw)
        return parsed if isinstance(parsed, list) else []
    except json.JSONDecodeError:
        return []
