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

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

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

from bson import ObjectId

from common.memos_core.schemas import (
    MemoCreate,
    MemoUpdate,
    MemoResponse,
)
from common.memos_core.services import get_memo_service, get_memo_relation_service
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),
    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)
        
        # Parse Dates from start_date/end_date params
        from datetime import datetime, timedelta, timezone
        import re
        
        dt_start = None
        dt_end = None
        
        if start_date:
            try:
                dt_start = datetime.fromisoformat(start_date.replace("Z", "+00:00"))
            except ValueError:
                pass # Ignore invalid format
        
        if end_date:
            try:
                dt_end = datetime.fromisoformat(end_date.replace("Z", "+00:00"))
            except ValueError:
                pass

        # Parse 'filter' query param (overrides start/end if present and matches)
        if filter:
            logger.debug("List memos GET with filter=%r", filter)
            # Case 1: Complex Timestamp Filter (created_ts >= ...)
            pattern_ts = r"(?:created_ts|updated_ts)\s*>=\s*([\d\.]+)\s*&&\s*(?:created_ts|updated_ts)\s*<\s*([\d\.]+)"
            match_ts = re.search(pattern_ts, filter)
            
            # Case 2: Frontend DisplayTime Filter (displayTime:2026-01-24)
            pattern_dt = r"displayTime:(\d{4}-\d{2}-\d{2})"
            match_dt = re.search(pattern_dt, filter)

            if match_ts:
                try:
                    ts_start = float(match_ts.group(1))
                    ts_end = float(match_ts.group(2))
                    dt_start = datetime.fromtimestamp(ts_start, tz=timezone.utc)
                    dt_end = datetime.fromtimestamp(ts_end, tz=timezone.utc)
                except (ValueError, TypeError):
                    pass
            elif match_dt:
                try:
                    date_str = match_dt.group(1)
                    # Parse YYYY-MM-DD
                    dt = datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
                    # Set range for that full day
                    dt_start = dt
                    dt_end = dt + timedelta(days=1)
                except ValueError:
                    pass

        return await memo_service.list_memos(
            user_id=user_id, 
            tag=tag,
            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", "")
        
        if raw_filter and isinstance(raw_filter, str):
            import re
            from datetime import datetime, timedelta, timezone

            logger.debug("List memos POST with raw_filter=%r", raw_filter)
            # Case 1: Complex Timestamp Filter (created_ts >= ...)
            pattern_ts = r"(?:created_ts|updated_ts)\s*>=\s*([\d\.]+)\s*&&\s*(?:created_ts|updated_ts)\s*<\s*([\d\.]+)"
            match_ts = re.search(pattern_ts, raw_filter)
            
            # Case 2: Frontend DisplayTime Filter (displayTime:2026-01-24)
            pattern_dt = r"displayTime:(\d{4}-\d{2}-\d{2})"
            match_dt = re.search(pattern_dt, raw_filter)
            # Case 3: Creator ID Filter (creator_id == ...)
            # Supports alphanumeric, hyphen, underscore, dot
            pattern_creator = r"creator_id\s*==\s*([a-zA-Z0-9_\-\.]+)"
            match_creator = re.search(pattern_creator, raw_filter)

            if match_ts:
                try:
                    ts_start = float(match_ts.group(1))
                    ts_end = float(match_ts.group(2))
                    start_date = datetime.fromtimestamp(ts_start, tz=timezone.utc)
                    end_date = datetime.fromtimestamp(ts_end, tz=timezone.utc)
                except (ValueError, TypeError):
                    pass
            elif match_dt:
                try:
                    date_str = match_dt.group(1)
                    # Parse YYYY-MM-DD
                    dt = datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
                    # Set range for that full day
                    start_date = dt
                    end_date = dt + timedelta(days=1)
                except ValueError:
                    pass
            
            if match_creator:
                creator_id = match_creator.group(1)
            else:
                creator_id = None
        else:
            creator_id = None

        return await memo_service.list_memos(
            user_id=user_id, 
            creator_id=creator_id,
            tag=tag,
            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.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 anonymous users: build synthetic creator_id
        if user_id is None:
            # Anonymous user: use anonymous_id from payload 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:
                # Sanitize display name (remove special chars that could break queries)
                sanitized_name = re.sub(r'[^a-zA-Z0-9_-]', '', anonymous_name)[:20]  # Limit length
                synthetic_user_id = f"{anonymous_id}_{sanitized_name}"
            else:
                synthetic_user_id = anonymous_id
            
            user_id = synthetic_user_id
        
        # 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"
        )
        
        # 4. TODO: Create Activity and Inbox notification
        # (if comment is not PRIVATE and creator != original memo creator)
        # This can be implemented later
        
        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 user has access
        memo = await memo_service.get_memo(memo_id, user_id=user_id)
        if not memo:
            raise HTTPException(status_code=404, detail=f"Memo {memo_id} not found")
        
        # 2. Get the actual _id of the memo for relation query and get memo owner
        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")
        
        related_memo_id = str(memo_doc["_id"])
        memo_owner_id = memo_doc.get("creator_id")
        
        # 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
