"""
Notes API for GetBiji.

Provides comprehensive note management including creation, listing,
and retrieval of notes with support for various note types.
"""

import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional

import requests

from .exceptions import UploadError, ValidationError
from .models import ChildrenCountResult, LinkNoteDetail, Note, NoteListResult, UploadTokenResult
from .stream_parser import StreamCallback, parse_sse_response
from .utils import (
    calculate_file_size,
    calculate_md5,
    format_json_content,
    generate_request_id,
    get_audio_duration_ms,
    get_file_extension,
    validate_audio_file,
)

if TYPE_CHECKING:
    from .client import GetBijiClient


class NotesAPI:
    """
    API for note operations.

    Supports creating and managing notes of various types:
    - Plain text notes (manual)
    - Link notes (AI-generated from URLs)
    - Audio notes (AI-generated from uploaded audio)
    """

    # API endpoints
    NOTES_LIST_URL = "https://get-notes.luojilab.com/voicenotes/web/notes"
    NOTES_CREATE_URL = "https://get-notes.luojilab.com/voicenotes/web/notes"
    NOTES_STREAM_URL = "https://get-notes.luojilab.com/voicenotes/web/notes/stream"
    NOTES_AUDIO_STREAM_URL = (
        "https://get-notes.luojilab.com/voicenotes/web/notes/stream_on_local_audio"
    )
    NOTES_AUDIO_TOKEN_URL = "https://get-notes.luojilab.com/voicenotes/web/notes/local_audio/token"
    LINK_DETAIL_URL = "https://get-notes.luojilab.com/voicenotes/web/notes/{}/links/detail"
    NOTES_EDIT_URL = "https://get-notes.luojilab.com/voicenotes/web/notes/{}"
    NOTES_ORIGINAL_URL = "https://get-notes.luojilab.com/voicenotes/web/topics/{}/notes/{}/original"
    NOTES_CHILDREN_COUNT_URL = (
        "https://get-notes.luojilab.com/voicenotes/web/notes/{}/children/count"
    )
    NOTES_GENERATING_COUNT_URL = (
        "https://get-notes.luojilab.com/voicenotes/web/notes/generating-count"
    )
    NOTES_COUNT_URL = "https://get-notes.luojilab.com/voicenotes/web/notes/count"
    NOTES_EXPORT_OPTIONS_URL = "https://get-notes.luojilab.com/voicenotes/web/notes/export-options"
    NOTES_TOPIC_DETAIL_URL = "https://get-notes.luojilab.com/voicenotes/web/topics/{}/notes/{}"

    def __init__(self, client: "GetBijiClient"):
        """
        Initialize NotesAPI.

        Args:
            client: GetBijiClient instance
        """
        self._client = client

    def create_simple_note(
        self,
        content: str,
        title: str = "",
        tags: Optional[List[str]] = None,
    ) -> Note:
        """
        Create a simple plain text note.

        Args:
            content: Note content (required)
            title: Note title (optional, will be generated if not provided)
            tags: List of tag names (optional)

        Returns:
            Created Note object

        Raises:
            ValidationError: If content is empty
            APIError: If API returns an error

        Example:
            >>> note = client.notes.create_simple_note(
            ...     content="Today I learned Python programming",
            ...     tags=["learning", "programming"]
            ... )
            >>> print(f"Created note: {note.id}")
        """
        if not content or not content.strip():
            raise ValidationError("Note content cannot be empty")

        # Build JSON content for rich text
        json_content = format_json_content(content)

        # Build tag objects if tags provided
        tag_objects = []
        if tags:
            for tag_name in tags:
                tag_objects.append(
                    {
                        "name": tag_name,
                        "type": "text",
                        "tag_type": "text",
                    }
                )

        payload = {
            "title": title or "",
            "content": content,
            "json_content": json_content,
            "entry_type": "manual",
            "note_type": "plain_text",
            "source": "web",
        }

        if tag_objects:
            payload["tags"] = tag_objects

        response = self._client.post(self.NOTES_CREATE_URL, json=payload)
        data = self._client._parse_response(response)
        note_data = data.get("c", {})

        return Note.from_dict(note_data)

    def create_link_note(
        self,
        url: str,
        callback: Optional[Callable[[int, Dict[str, Any]], None]] = None,
        prompt_template_id: Optional[str] = None,
    ) -> Note:
        """
        Create a note from a URL (AI-generated, streaming response).

        This method creates a note by processing a web URL. The GetBiji AI will
        browse the link, analyze the content, and generate a comprehensive note.

        Args:
            url: The URL to process
            callback: Optional callback function for progress updates
                Called with (msg_type, data) during streaming:
                - msg_type=-1: Initialization with note_id
                - msg_type=6: Progress updates
                - msg_type=-2: Final note completion
            prompt_template_id: Optional prompt template ID

        Returns:
            Created Note object with AI-generated content

        Raises:
            ValidationError: If URL is empty
            APIError: If API returns an error

        Example:
            >>> def on_progress(msg_type, data):
            ...     if msg_type == 6:
            ...         print(f"Processing: {data}")
            >>> note = client.notes.create_link_note(
            ...     url="https://example.com/article",
            ...     callback=on_progress
            ... )
        """
        if not url or not url.strip():
            raise ValidationError("URL cannot be empty")

        payload = {
            "attachments": [
                {
                    "type": "link",
                    "url": url,
                }
            ],
            "content": "",
            "entry_type": "ai",
            "note_type": "link",
            "source": "web",
        }

        if prompt_template_id:
            payload["prompt_template_id"] = prompt_template_id

        response = self._client.post(
            self.NOTES_STREAM_URL,
            json=payload,
            stream=True,
        )

        # Handle streaming response
        final_note = None
        note_id = None

        for msg_type, data in parse_sse_response(response):
            if msg_type == -1:
                # Initialization
                note_id = data.get("note_id")
            elif msg_type == 6:
                # Progress update
                if callback:
                    callback(msg_type, data)
            elif msg_type == -2:
                # Final result
                final_note = Note.from_dict(data)
                break

        if final_note is None:
            raise UploadError("Failed to create link note from streaming response")

        return final_note

    def create_audio_note(
        self,
        audio_path: str,
        callback: Optional[Callable[[int, Dict[str, Any]], None]] = None,
        topic_id: str = "0",
        prompt_template_id: Optional[str] = None,
    ) -> Note:
        """
        Create a note from an audio file (AI-generated, streaming response).

        This is a three-step process:
        1. Get OSS upload token with file info
        2. Upload the audio file to OSS
        3. Generate note using AI (streaming)

        Args:
            audio_path: Path to the audio file
            callback: Optional callback for progress updates
            topic_id: Knowledge base ID to add note to (default: "0" = none)
            prompt_template_id: Optional prompt template ID

        Returns:
            Created Note object with AI-generated transcript and summary

        Raises:
            ValidationError: If file doesn't exist or is invalid
            UploadError: If upload fails

        Example:
            >>> note = client.notes.create_audio_note(
            ...     audio_path="/path/to/recording.mp3",
            ...     callback=lambda t, d: print(f"Progress: {t}")
            ... )
        """
        # Validate file
        if not os.path.exists(audio_path):
            raise ValidationError(f"Audio file not found: {audio_path}")

        if not validate_audio_file(audio_path):
            raise ValidationError(f"Invalid audio file: {audio_path}")

        # Step 1: Get OSS upload token
        file_size = calculate_file_size(audio_path)
        md5_hash = calculate_md5(audio_path)
        file_ext = get_file_extension(audio_path)

        duration_ms = get_audio_duration_ms(audio_path)

        token_payload = {
            "duration_ms": duration_ms,
            "local_name": os.path.basename(audio_path),
            "md5": md5_hash,
            "size_byte": file_size,
            "type": file_ext,
        }

        token_response = self._client.post(self.NOTES_AUDIO_TOKEN_URL, json=token_payload)
        token_data = self._client._parse_response(token_response)
        upload_result = UploadTokenResult.from_dict(token_data.get("c", {}))

        file_id = upload_result.file_id
        audio_url = None

        # Step 2: Upload to OSS (if not already uploaded)
        if not upload_result.is_uploaded and upload_result.token_info:
            token_info = upload_result.token_info

            # Read file content
            with open(audio_path, "rb") as f:
                file_content = f.read()

            # Upload to OSS
            oss_headers = {
                "Content-MD5": md5_hash,
                "Content-Type": token_info.put_content_type,
            }

            upload_response = requests.put(
                token_info.put_sign_url,
                data=file_content,
                headers=oss_headers,
            )

            if upload_response.status_code != 200:
                raise UploadError(f"OSS upload failed: {upload_response.status_code}")

            audio_url = token_info.get_url
        elif upload_result.is_uploaded and upload_result.image_path:
            # File was already uploaded
            audio_url = upload_result.image_path

        if not audio_url:
            # Fallback: use the token info get_url or construct from file_id
            if upload_result.token_info:
                audio_url = upload_result.token_info.get_url
            else:
                audio_url = f"file://{file_id}"

        # Step 3: Generate note via streaming
        stream_payload = {
            "note_id": "0",
            "file_id": file_id,
            "attachments": [
                {
                    "type": "audio",
                    "url": audio_url,
                    "duration": duration_ms,
                }
            ],
            "entry_type": "ai",
            "note_type": "local_audio",
            "source": "web",
            "topic_id": topic_id,
        }

        if prompt_template_id:
            stream_payload["prompt_template_id"] = prompt_template_id

        stream_response = self._client.post(
            self.NOTES_AUDIO_STREAM_URL,
            json=stream_payload,
            stream=True,
        )

        # Handle streaming response
        final_note = None

        for msg_type, data in parse_sse_response(stream_response):
            if msg_type == -1:
                # Initialization with note_id
                if callback:
                    callback(msg_type, data)
            elif msg_type == -6:
                # Chapter progress (audio note specific)
                if callback:
                    callback(msg_type, data)
            elif msg_type == -2:
                # Final result
                final_note = Note.from_dict(data)
                break

        if final_note is None:
            raise UploadError("Failed to create audio note from streaming response")

        return final_note

    def list_notes(
        self,
        limit: int = 10,
        since_id: Optional[str] = None,
        sort: str = "create_desc",
    ) -> NoteListResult:
        """
        List notes with pagination.

        Args:
            limit: Number of notes per page (default: 10)
            since_id: Pagination cursor (note_id from last note in previous page)
            sort: Sort order (default: "create_desc" for newest first)

        Returns:
            NoteListResult containing:
            - list: List of Note objects
            - total_items: Total number of notes
            - has_more: Whether there are more pages
            - limit: Current page limit

        Raises:
            APIError: If API returns an error

        Example:
            >>> result = client.notes.list_notes(limit=20)
            >>> for note in result.list:
            ...     print(f"{note.title}: {note.content[:50]}...")
            >>>
            >>> # Get next page
            >>> if result.has_more:
            ...     next_result = client.notes.list_notes(
            ...         limit=20,
            ...         since_id=result.list[-1].id
            ...     )
        """
        params = {
            "limit": limit,
            "sort": sort,
        }

        if since_id:
            params["since_id"] = since_id

        response = self._client.get(self.NOTES_LIST_URL, params=params)
        data = response.get("c", {})

        return NoteListResult.from_dict(data)

    def get_note(self, note_id: str) -> Note:
        """
        Get note details by ID.

        Args:
            note_id: The note ID

        Returns:
            Note object with full details

        Raises:
            ValidationError: If note_id is empty
            APIError: If API returns an error

        Example:
            >>> note = client.notes.get_note("abc123")
            >>> print(f"{note.title}\\n{note.content}")
        """
        if not note_id:
            raise ValidationError("Note ID cannot be empty")

        url = f"{self.NOTES_LIST_URL}/{note_id}"
        response = self._client.get(url)
        data = response.get("c", {})

        return Note.from_dict(data)

    def get_link_note_content(self, note_id: str) -> LinkNoteDetail:
        """
        Get the original content of a link-type note.

        This retrieves the original web page content that was used
        to generate the link note.

        Args:
            note_id: The note ID (must be a link-type note)

        Returns:
            LinkNoteDetail with original content, URL, and metadata

        Raises:
            ValidationError: If note_id is empty
            APIError: If API returns an error

        Example:
            >>> detail = client.notes.get_link_note_content("abc123")
            >>> print(f"Original title: {detail.web_title}")
            >>> print(f"Source URL: {detail.url}")
        """
        if not note_id:
            raise ValidationError("Note ID cannot be empty")

        url = self.LINK_DETAIL_URL.format(note_id)
        response = self._client.get(url)
        data = response.get("c", {})

        return LinkNoteDetail.from_dict(data)

    def get_audio_note_sections(self, note: Note) -> List[Dict[str, Any]]:
        """
        Extract chapter/section information from an audio note.

        Audio notes contain timestamped chapters in the content.
        This method parses and extracts them.

        Args:
            note: The Note object (must be audio type)

        Returns:
            List of chapter info dicts with:
            - title: Chapter title
            - timestamp: Formatted timestamp (HH:MM:SS)
            - milliseconds: Timestamp in milliseconds
            - description: Chapter description (if available)

        Example:
            >>> note = client.notes.get_note("audio_note_id")
            >>> sections = client.notes.get_audio_note_sections(note)
            >>> for section in sections:
            ...     print(f"{section['timestamp']}: {section['title']}")
        """
        from .stream_parser import parse_audio_note_sections

        if note.note_type != "local_audio":
            return []

        return [
            {
                "title": s.title,
                "description": s.description,
                "timestamp": s.timestamp,
                "milliseconds": s.milliseconds,
            }
            for s in parse_audio_note_sections(note.content)
        ]

    def add_note_to_knowledge_base(
        self,
        note_id: str,
        topic_id: str,
    ) -> Note:
        """
        Add an existing note to a knowledge base.

        This creates a new note in the knowledge base by copying
        the content of an existing note.

        Args:
            note_id: The source note ID
            topic_id: The target knowledge base ID

        Returns:
            The newly created Note in the knowledge base

        Raises:
            ValidationError: If IDs are empty
            APIError: If API returns an error

        Example:
            >>> note = client.notes.add_note_to_knowledge_base(
            ...     note_id="abc123",
            ...     topic_id="456"
            ... )
        """
        if not note_id or not topic_id:
            raise ValidationError("Note ID and Topic ID are required")

        # Get the original note
        original_note = self.get_note(note_id)

        # Create a new note in the knowledge base
        # This is done by creating a note with topic_id
        # The API endpoint may vary - this is a common approach
        payload = {
            "title": original_note.title,
            "content": original_note.content,
            "json_content": original_note.json_content,
            "entry_type": original_note.entry_type,
            "note_type": original_note.note_type,
            "source": "knowledge",
            "topic_id": topic_id,
        }

        response = self._client.post(self.NOTES_CREATE_URL, json=payload)
        data = self._client._parse_response(response)
        note_data = data.get("c", {})

        return Note.from_dict(note_data)

    def edit_note(
        self,
        note_id: str,
        title: str,
        content: str,
        json_content: str,
        version: int,
        note_type: str = "plain_text",
        tags: Optional[List[Dict[str, Any]]] = None,
        attachments: Optional[List[Dict[str, Any]]] = None,
    ) -> Note:
        """
        Edit an existing note (full replacement).

        Args:
            note_id: The note ID to edit
            title: Updated title
            content: Updated Markdown content (full replacement, not incremental)
            json_content: Updated rich text JSON content
            version: Current version number (optimistic locking)
            note_type: Note type (plain_text, local_audio, link, img_text)
            tags: List of tag dicts to submit (must include all existing tags)
            attachments: List of attachment dicts to submit (must include all existing)

        Returns:
            Updated Note object (version will be incremented by 1)

        Raises:
            ValidationError: If note_id or content is empty
            APIError: If API returns an error
        """
        if not note_id:
            raise ValidationError("Note ID cannot be empty")
        if not content or not content.strip():
            raise ValidationError("Note content cannot be empty")

        payload: Dict[str, Any] = {
            "note_id": note_id,
            "title": title,
            "content": content,
            "json_content": json_content,
            "entry_type": "manual",
            "note_type": note_type,
            "source": "web",
            "version": version,
        }

        if tags is not None:
            payload["tags"] = tags
        if attachments is not None:
            payload["attachments"] = attachments

        url = self.NOTES_EDIT_URL.format(note_id)
        response = self._client.put(url, json=payload)
        note_data = response.get("c", {})

        return Note.from_dict(note_data)

    def get_note_in_topic(self, topic_id: str, note_id: str) -> Note:
        """
        Get note details within a knowledge base context.

        Args:
            topic_id: The knowledge base ID
            note_id: The note ID

        Returns:
            Note object with knowledge base context

        Raises:
            ValidationError: If IDs are empty
            APIError: If API returns an error
        """
        if not topic_id or not note_id:
            raise ValidationError("Topic ID and Note ID are required")

        url = self.NOTES_TOPIC_DETAIL_URL.format(topic_id, note_id)
        response = self._client.get(url, params={"from": "GETNOTE"})
        data = response.get("c", {})

        return Note.from_dict(data)

    def get_audio_original(self, topic_id: str, note_id: str) -> Dict[str, Any]:
        """
        Get the original transcript text of an audio note.

        Args:
            topic_id: The knowledge base ID
            note_id: The note ID (must be a local_audio type note)

        Returns:
            Dict containing the original transcript data

        Raises:
            ValidationError: If IDs are empty
            APIError: If API returns an error
        """
        if not topic_id or not note_id:
            raise ValidationError("Topic ID and Note ID are required")

        url = self.NOTES_ORIGINAL_URL.format(topic_id, note_id)
        response = self._client.get(url, params={"from": "GETNOTE"})
        return response.get("c", {})

    def get_children_count(self, note_id: str) -> ChildrenCountResult:
        """
        Get the count of child notes for a given note.

        Args:
            note_id: The note ID

        Returns:
            ChildrenCountResult with total, can_add_child_note, max_child_note_count

        Raises:
            ValidationError: If note_id is empty
            APIError: If API returns an error
        """
        if not note_id:
            raise ValidationError("Note ID cannot be empty")

        url = self.NOTES_CHILDREN_COUNT_URL.format(note_id)
        response = self._client.get(url)
        return ChildrenCountResult.from_dict(response.get("c", {}))

    def get_generating_count(self) -> int:
        """
        Get the count of notes currently being AI-generated.

        Returns:
            Number of notes currently being generated
        """
        response = self._client.get(self.NOTES_GENERATING_COUNT_URL)
        return response.get("c", {}).get("count", 0)

    def get_notes_count(self) -> int:
        """
        Get the total count of user's notes.

        Returns:
            Total number of notes
        """
        response = self._client.get(self.NOTES_COUNT_URL)
        return response.get("c", {}).get("count", 0)

    def get_export_options(self) -> Dict[str, Any]:
        """
        Get available note export options.

        Returns:
            Dict containing export options configuration
        """
        response = self._client.get(self.NOTES_EXPORT_OPTIONS_URL)
        return response.get("c", {})
