"""
Memo service routes for Memos-style backend.
"""

from typing import List, Any
import logging
import re
import secrets

from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request

from bson import ObjectId

from common.memos_core.schemas import (
    MemoCreate,
    MemoUpdate,
    MemoResponse,
    MemoVersionResponse,
)
from common.memos_core.services import get_memo_service, get_memo_relation_service
from common.memos_core.query_parser import parse_date_range
from common.mongo_client import mongodb_client


logger = logging.getLogger(__name__)
router = APIRouter(prefix="/memos")


def get_current_user_id(request: Request) -> str | None:
    """Extract user_id from request.state (set by auth middleware)."""
    return getattr(request.state, "user_id", None)


@router.get("", summary="List memos", response_model=List[MemoResponse])
async def list_memos(
    request: Request,
    tag: str | None = Query(default=None),
    filter: str | None = Query(default=None),
    row_status: str | None = Query(default=None, description="Filter by status: NORMAL or ARCHIVED"),
    start_date: str | None = Query(default=None, description="ISO format date (YYYY-MM-DD)"),
    end_date: str | None = Query(default=None, description="ISO format date (YYYY-MM-DD)"),
    memo_service=Depends(get_memo_service),
):
    """List memos for the current user (or anonymous if not logged in)."""
    try:
        user_id = get_current_user_id(request)
        is_auth = getattr(request.state, "is_authenticated", False)
        logger.warning("🔍 LIST_MEMOS: user_id=%s, is_authenticated=%s, has_auth_header=%s", 
                       user_id, is_auth, bool(request.headers.get("authorization")))
        
        # Parse date range from query params or filter string
        dt_start, dt_end = parse_date_range(
            start_date=start_date,
            end_date=end_date,
            filter_str=filter,
        )
        
        # Parse creator_id from filter string (e.g. "creator_id == user_xxx")
        creator_id = None
        if filter:
            logger.debug("List memos GET with filter=%r", filter)
            pattern_creator = r"creator_id\s*==\s*([a-zA-Z0-9_\-\.]+)"
            match_creator = re.search(pattern_creator, filter)
            if match_creator:
                creator_id = match_creator.group(1)

        # Parse pinned from filter string (e.g. "... && pinned")
        pinned = None
        if filter and re.search(r'\bpinned\b(?!\s*==\s*false)', filter):
            pinned = True

        return await memo_service.list_memos(
            user_id=user_id, 
            creator_id=creator_id,
            tag=tag,
            pinned=pinned,
            row_status=row_status,
            start_date=dt_start,
            end_date=dt_end
        )
    except Exception as exc:  # pragma: no cover
        raise HTTPException(status_code=500, detail=str(exc)) from exc


@router.post("", summary="Create memo (Connect compatibility)")
async def create_memo_or_list_memos(
    request: Request,
    payload: dict = Body(default_factory=dict),  # noqa: B008
    memo_service=Depends(get_memo_service),
):
    """
    Vite dev proxy maps multiple Connect RPCs to the same REST path `/api/v1/memos`:
    - MemoService/ListMemos  -> POST /api/v1/memos  (should be GET but method can't be rewritten)
    - MemoService/CreateMemo -> POST /api/v1/memos

    To avoid 422 and keep dev UX smooth, accept an untyped payload and branch by shape.
    """
    try:
        user_id = get_current_user_id(request)
        raw = payload if isinstance(payload, dict) else {}

        # Connect CreateMemo often wraps payload as { "memo": { ... } }
        if isinstance(raw.get("memo"), dict):
            memo_create = MemoCreate(**raw["memo"])
            return await memo_service.create_memo(memo_create, user_id=user_id)

        # If it looks like a create payload, also treat as create.
        if "content" in raw:
            memo_create = MemoCreate(**raw)
            return await memo_service.create_memo(memo_create, user_id=user_id)

        # Otherwise treat as ListMemos.
        # Support basic tag filter when provided.
        tag = raw.get("tag") if isinstance(raw.get("tag"), str) else None
        
        # Parse filter string from frontend (e.g. "created_ts >= 1737676800 && created_ts < 1737763200")
        start_date = None
        end_date = None
        raw_filter = raw.get("filter", "")
        
        creator_id = None
        if raw_filter and isinstance(raw_filter, str):
            logger.debug("List memos POST with raw_filter=%r", raw_filter)
            
            # Parse date range from filter string
            start_date, end_date = parse_date_range(filter_str=raw_filter)
            
            # Parse creator_id filter (creator_id == ...)
            pattern_creator = r"creator_id\s*==\s*([a-zA-Z0-9_\-\.]+)"
            match_creator = re.search(pattern_creator, raw_filter)
            if match_creator:
                creator_id = match_creator.group(1)

        # Parse pinned filter — frontend sends "pinned" in filter string for bookmarks
        pinned = None
        if raw_filter and isinstance(raw_filter, str):
            # Match standalone "pinned" (not "pinned == false")
            if re.search(r'\bpinned\b(?!\s*==\s*false)', raw_filter):
                pinned = True

        return await memo_service.list_memos(
            user_id=user_id, 
            creator_id=creator_id,
            tag=tag,
            pinned=pinned,
            start_date=start_date,
            end_date=end_date
        )
    except Exception as exc:  # pragma: no cover
        raise HTTPException(status_code=400, detail=str(exc)) from exc


@router.get("/{memo_id}", summary="Get memo by ID", response_model=MemoResponse)
async def get_memo(
    request: Request,
    memo_id: str, 
    memo_service=Depends(get_memo_service)
):
    """Get a specific memo (only if owned by current user or public)."""
    try:
        user_id = get_current_user_id(request)
        return await memo_service.get_memo(memo_id, user_id=user_id)
    except Exception as exc:  # pragma: no cover
        raise HTTPException(status_code=404, detail=str(exc)) from exc


@router.patch("/{memo_id}", summary="Update memo", response_model=MemoResponse)
async def update_memo(
    request: Request,
    memo_id: str,
    payload: MemoUpdate,
    memo_service=Depends(get_memo_service),
):
    """Update a memo (only if owned by current user)."""
    try:
        user_id = get_current_user_id(request)
        return await memo_service.update_memo(memo_id, payload, user_id=user_id)
    except Exception as exc:  # pragma: no cover
        raise HTTPException(status_code=400, detail=str(exc)) from exc


@router.delete("/{memo_id}", summary="Delete memo")
async def delete_memo(
    request: Request,
    memo_id: str, 
    memo_service=Depends(get_memo_service)
):
    """Delete a memo (only if owned by current user)."""
    try:
        user_id = get_current_user_id(request)
        await memo_service.delete_memo(memo_id, user_id=user_id)
        return {"status": "success"}
    except Exception as exc:  # pragma: no cover
        raise HTTPException(status_code=400, detail=str(exc)) from exc


@router.get("/{memo_id}/versions", summary="Get memo version history", response_model=MemoVersionResponse)
async def get_memo_versions(
    request: Request,
    memo_id: str,
    memo_service=Depends(get_memo_service),
):
    """Get version history for a memo."""
    try:
        user_id = get_current_user_id(request)
        return await memo_service.get_memo_versions(memo_id, user_id=user_id)
    except Exception as exc:  # pragma: no cover
        raise HTTPException(status_code=404, detail=str(exc)) from exc


@router.post("/{memo_id}/comments", summary="Create comment", response_model=MemoResponse)
async def create_memo_comment(
    request: Request,
    memo_id: str,
    payload: MemoCreate,
    memo_service=Depends(get_memo_service),
    memo_relation_service=Depends(get_memo_relation_service),
):
    """
    Create a comment for a memo.
    - Creates a new memo with parent = memo_id
    - Creates a MemoRelation with type = COMMENT
    - Returns the created comment
    """
    try:
        user_id = get_current_user_id(request)
        
        # 1. Verify original memo exists and user has access
        original_memo = await memo_service.get_memo(memo_id, user_id=user_id)
        if not original_memo:
            raise HTTPException(status_code=404, detail=f"Memo {memo_id} not found")
        
        # 2. Get the actual _id of the parent memo for consistent storage
        # This ensures parent field always uses _id string (not uid)
        from bson import ObjectId
        
        parent_doc = None
        if ObjectId.is_valid(memo_id):
            parent_doc = await mongodb_client.memos.find_one({"_id": ObjectId(memo_id)})
        if not parent_doc:
            parent_doc = await mongodb_client.memos.find_one({"uid": memo_id})
        
        if not parent_doc:
            raise HTTPException(status_code=404, detail=f"Parent memo {memo_id} not found")
        
        # Use _id string as parent (standardized format)
        parent_id_str = str(parent_doc["_id"])
        
        # 3. Handle users: prioritize anonymous_id/anonymous_name from payload if provided
        # This allows custom display names even when DISABLE_AUTH=true
        if payload.anonymous_id or payload.anonymous_name:
            # Use provided anonymous_id or generate one
            anonymous_id = payload.anonymous_id or f"anonymous_{secrets.token_hex(4)}"
            anonymous_name = payload.anonymous_name
            
            # Build creator_id: anonymous_<sessionId> or anonymous_<sessionId>_<displayName>
            if anonymous_name:
                # URL-safe base64 encode the display name to preserve Unicode characters
                # This allows Vietnamese, Chinese, etc. names to be stored and displayed
                import base64
                encoded_name = base64.urlsafe_b64encode(anonymous_name.encode('utf-8')).decode('ascii')
                # Limit to 50 chars max (base64 expands size)
                encoded_name = encoded_name[:50].rstrip('=')
                synthetic_user_id = f"{anonymous_id}_{encoded_name}"
            else:
                synthetic_user_id = anonymous_id
            
            user_id = synthetic_user_id
        elif user_id is None:
            # No auth and no anonymous info provided - generate anonymous ID
            user_id = f"anonymous_{secrets.token_hex(4)}"

        
        # 4. Create comment memo (with parent = _id string)
        # Allow anonymous users to choose PRIVATE comments
        # Store the base anonymous_id for later filtering
        base_anonymous_id = None
        if user_id and user_id.startswith("anonymous_"):
            # Extract base anonymous_id (before optional display name)
            # Format: anonymous_xxx or anonymous_xxx_name
            parts = user_id.split("_", 2)
            if len(parts) >= 2:
                base_anonymous_id = f"{parts[0]}_{parts[1]}"  # anonymous_xxx
            else:
                base_anonymous_id = user_id
            # Allow anonymous to choose visibility (PRIVATE or PUBLIC)
            comment_visibility = payload.visibility or "PUBLIC"
        else:
            comment_visibility = payload.visibility or "PUBLIC"
        
        comment_payload = MemoCreate(
            content=payload.content,
            visibility=comment_visibility,
            tags=payload.tags or [],
            create_time=payload.create_time
        )
        
        # Store parent as _id string for consistent querying
        # Pass base_anonymous_id to service for storage
        comment = await memo_service.create_memo(
            comment_payload, 
            user_id=user_id,
            parent_id=parent_id_str,
            anonymous_id=base_anonymous_id
        )
        
        # 4. Create MemoRelation (for referential integrity and quick queries)
        # Relation: comment -> original memo
        # Use _id strings for both memo_id and related_memo_id
        await memo_relation_service.create_relation(
            memo_id=comment.id,  # comment's _id
            related_memo_id=parent_id_str,  # parent's _id
            relation_type="COMMENT"
        )
        
        # 5. Create Activity and Notification for memo owner
        # Only if: comment is PUBLIC and commenter is different from original memo creator
        try:
            parent_creator_id = parent_doc.get("creator_id")
            
            # Only notify if: 
            # 1. Comment is PUBLIC (owner should see it)
            # 2. Commenter is not the memo owner
            if comment_visibility == "PUBLIC" and parent_creator_id and parent_creator_id != user_id:
                from common.memos_core.services import ActivityService, NotificationService
                
                activity_service = ActivityService()
                notification_service = NotificationService()
                
                # Create activity record
                activity = await activity_service.create_activity(
                    activity_type="MEMO_COMMENT",
                    creator_id=user_id,
                    memo_id=comment.id,  # The comment memo
                    related_memo_id=parent_id_str  # The original memo
                )
                
                # Extract activity ID from name (activities/123456 -> 123456)
                activity_id = int(activity.name.split("/")[-1])
                
                # Create notification for memo owner
                await notification_service.create_notification(
                    recipient_id=parent_creator_id,
                    sender_id=user_id,
                    activity_id=activity_id,
                    notification_type="MEMO_COMMENT"
                )
        except Exception as notify_exc:
            # Don't fail the comment creation if notification fails
            # Just log and continue
            import logging
            logging.warning(f"Failed to create notification: {notify_exc}")
        
        return comment
        
    except HTTPException:
        raise
    except Exception as exc:  # pragma: no cover
        raise HTTPException(status_code=400, detail=str(exc)) from exc


@router.get("/{memo_id}/comments", summary="List comments", response_model=dict)
async def list_memo_comments(
    request: Request,
    memo_id: str,
    anonymous_id: str | None = Query(None, description="Anonymous user ID for filtering PRIVATE comments"),
    memo_service=Depends(get_memo_service),
    memo_relation_service=Depends(get_memo_relation_service),
):
    """
    List all comments for a memo.
    - Uses MemoRelation query (like memos original)
    - Queries relations with related_memo_id = memo_id and type = COMMENT
    - Fetches memos by IDs from relations
    - Applies visibility filter based on current user
    - Sorted by create_time ASC (oldest first)
    """
    try:
        user_id = get_current_user_id(request)
        
        # 1. Verify memo exists and get raw doc in ONE query
        memo_doc = None
        if ObjectId.is_valid(memo_id):
            memo_doc = await mongodb_client.memos.find_one({"_id": ObjectId(memo_id)})
        if not memo_doc:
            memo_doc = await mongodb_client.memos.find_one({"uid": memo_id})
        
        if not memo_doc:
            raise HTTPException(status_code=404, detail=f"Memo {memo_id} not found")
        
        # Access check: owner, PUBLIC, or PROTECTED (logged-in user)
        memo_visibility = memo_doc.get("visibility", "PRIVATE")
        memo_creator = memo_doc.get("creator_id", "anonymous")
        if memo_visibility == "PRIVATE" and memo_creator != user_id:
            raise HTTPException(status_code=404, detail=f"Memo {memo_id} not found")
        if memo_visibility == "PROTECTED" and (not user_id or user_id == "anonymous"):
            raise HTTPException(status_code=403, detail="Access denied: authentication required")
        
        related_memo_id = str(memo_doc["_id"])
        memo_owner_id = memo_creator
        
        # 3. Query MemoRelations with related_memo_id and type = COMMENT (like memos original)
        relations = await memo_relation_service.list_relations(
            memo_id=related_memo_id,
            related_memo_id=related_memo_id,
            relation_type="COMMENT"
        )
        
        if len(relations) == 0:
            return {
                "memos": [],
                "nextPageToken": "",
                "totalSize": 0
            }
        
        # 4. Extract memo_id list from relations
        comment_memo_ids = [r["memo_id"] for r in relations]
        
        # 5. Query memos by IDs
        # Convert string IDs to ObjectId for query
        comment_memo_object_ids = []
        for memo_id_str in comment_memo_ids:
            if ObjectId.is_valid(memo_id_str):
                comment_memo_object_ids.append(ObjectId(memo_id_str))
        
        if len(comment_memo_object_ids) == 0:
            return {
                "memos": [],
                "nextPageToken": "",
                "totalSize": 0
            }
        
        # 6. Query memos and apply visibility filter (like memos original)
        query = {
            "_id": {"$in": comment_memo_object_ids},
            "row_status": {"$ne": "ARCHIVED"}
        }
        
        # Visibility filter based on current user
        if user_id and user_id != "anonymous":
            # Logged-in users: see own memos + PUBLIC + PROTECTED
            visibility_conditions = [
                {"creator_id": user_id},
                {"visibility": "PUBLIC"},
                {"visibility": "PROTECTED"}
            ]
            
            # Memo owner can see all PRIVATE comments (not just their own)
            if memo_owner_id and user_id == memo_owner_id:
                visibility_conditions.append({"visibility": "PRIVATE"})
            
            query["$or"] = visibility_conditions
        else:
            # Anonymous users: PUBLIC + own PRIVATE comments (if anonymous_id provided)
            visibility_conditions: list[dict[str, Any]] = [{"visibility": "PUBLIC"}]
            
            # If anonymous_id provided, also show PRIVATE comments created by this anonymous user
            if anonymous_id:
                visibility_conditions.append({
                    "$and": [
                        {"visibility": "PRIVATE"},
                        {"anonymous_id": anonymous_id}
                    ]
                })
            
            if len(visibility_conditions) > 1:
                query["$or"] = visibility_conditions
            else:
                query["visibility"] = "PUBLIC"
        
        # 7. Query and sort by create_time ASC (oldest first)
        cursor = mongodb_client.memos.find(query).sort("created_at", 1)
        docs = await cursor.to_list(length=100)
        
        # 8. Convert to MemoResponse
        comments = [memo_service._doc_to_response(doc) for doc in docs]
        
        return {
            "memos": comments,
            "nextPageToken": "",
            "totalSize": len(comments)
        }
        
    except HTTPException:
        raise
    except Exception as exc:  # pragma: no cover
        raise HTTPException(status_code=400, detail=str(exc)) from exc
