"""
Granola API Client - Fetches data directly from Granola's API.

This client uses Granola's HTTP API instead of the local cache, which ensures
transcripts are always available (the local cache only has transcripts for
meetings that have been opened in the Granola app).

Auth tokens are read from:
~/Library/Application Support/Granola/supabase.json
"""

import json
import requests
import subprocess
import time
from pathlib import Path
from typing import Optional, Literal
from datetime import datetime
from dataclasses import dataclass


GRANOLA_API_BASE = "https://api.granola.ai/v1"
SUPABASE_CONFIG_PATH = Path.home() / "Library" / "Application Support" / "Granola" / "supabase.json"
GRANOLA_APP_PATH = Path("/Applications/Granola.app")
TOKEN_EXPIRY_BUFFER_SECONDS = 300  # Refresh 5 minutes before expiry


def _get_granola_version() -> str:
    """Get the installed Granola app version from Info.plist."""
    plist_path = GRANOLA_APP_PATH / "Contents" / "Info.plist"
    if plist_path.exists():
        try:
            import plistlib
            with open(plist_path, "rb") as f:
                plist = plistlib.load(f)
                return plist.get("CFBundleShortVersionString", "6.0.0")
        except Exception:
            pass
    return "6.0.0"  # Fallback version


@dataclass
class TranscriptEntry:
    """Single transcript entry"""
    timestamp: str
    speaker: str  # "You" or "Other"
    text: str


@dataclass
class MeetingSummary:
    """Summary of a meeting"""
    id: str
    title: str
    date: str
    date_formatted: str
    attendees: list[str]
    has_transcript: bool
    has_notes: bool
    notes_summary: Optional[str] = None


@dataclass
class MeetingSearchResult(MeetingSummary):
    """Meeting search result with match info"""
    matched_in: list[str] = None
    relevance_score: int = 0

    def __post_init__(self):
        if self.matched_in is None:
            self.matched_in = []


class GranolaAPIClient:
    """
    Client for fetching Granola meeting data via API.

    Unlike the cache-based client, this fetches transcripts directly from
    Granola's backend, ensuring all transcripts are available.
    """

    def __init__(self, config_path: Optional[Path] = None, auto_refresh: bool = True):
        """
        Initialize the API client.

        Args:
            config_path: Optional custom path to supabase.json
            auto_refresh: If True, automatically refresh expired tokens by opening Granola
        """
        self.config_path = config_path or SUPABASE_CONFIG_PATH
        self.auto_refresh = auto_refresh
        self._access_token = None
        self._token_expires_at = None
        self._documents_cache = None

    def _is_token_expired(self) -> bool:
        """Check if the current token is expired or about to expire."""
        if not self.config_path.exists():
            return True

        try:
            content = self.config_path.read_text()
            data = json.loads(content)
            workos_tokens = json.loads(data.get("workos_tokens", "{}"))

            obtained_at = workos_tokens.get("obtained_at", 0)  # milliseconds
            expires_in = workos_tokens.get("expires_in", 0)  # seconds

            # Calculate expiry time with buffer
            expires_at_ms = obtained_at + (expires_in * 1000)
            buffer_ms = TOKEN_EXPIRY_BUFFER_SECONDS * 1000
            now_ms = time.time() * 1000

            return now_ms >= (expires_at_ms - buffer_ms)
        except (json.JSONDecodeError, KeyError):
            return True

    def _refresh_token(self, max_wait: int = 10) -> bool:
        """
        Refresh the token by opening the Granola app.

        Args:
            max_wait: Maximum seconds to wait for token refresh

        Returns:
            True if token was refreshed, False otherwise
        """
        # Get the current file modification time
        old_mtime = self.config_path.stat().st_mtime if self.config_path.exists() else 0

        # Open Granola app
        try:
            subprocess.run(["open", "-a", "Granola"], check=True)
        except subprocess.CalledProcessError:
            return False

        # Wait for config file to be updated
        start = time.time()
        while time.time() - start < max_wait:
            time.sleep(0.5)
            if self.config_path.exists():
                new_mtime = self.config_path.stat().st_mtime
                if new_mtime > old_mtime:
                    # Token was refreshed, clear cached token
                    self._access_token = None
                    self._token_expires_at = None
                    return True

        return False

    def _load_access_token(self) -> str:
        """Load the WorkOS access token from config, refreshing if needed."""
        # Check if token needs refresh
        if self.auto_refresh and self._is_token_expired():
            if not self._refresh_token():
                raise PermissionError(
                    "Token expired and auto-refresh failed. "
                    "Please open the Granola app manually to refresh your token."
                )
            # Clear cached token to force reload
            self._access_token = None

        if self._access_token:
            return self._access_token

        if not self.config_path.exists():
            raise FileNotFoundError(
                f"Granola config not found at {self.config_path}. "
                "Is Granola installed and are you logged in?"
            )

        try:
            content = self.config_path.read_text()
            data = json.loads(content)
            workos_tokens = json.loads(data.get("workos_tokens", "{}"))
            self._access_token = workos_tokens.get("access_token")

            if not self._access_token:
                raise ValueError("No access token found in Granola config")

            return self._access_token
        except json.JSONDecodeError as e:
            raise ValueError(f"Failed to parse Granola config: {e}")

    def _make_request(self, endpoint: str, payload: dict = None, _retry: bool = True) -> dict | list:
        """Make an authenticated request to the Granola API."""
        token = self._load_access_token()

        url = f"{GRANOLA_API_BASE}/{endpoint}"
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "x-client-version": _get_granola_version(),
        }

        response = requests.post(url, headers=headers, json=payload or {})

        # Handle auth failures
        if response.status_code == 401:
            if self.auto_refresh and _retry:
                # Try refreshing token and retry once
                self._access_token = None
                if self._refresh_token():
                    return self._make_request(endpoint, payload, _retry=False)
            raise PermissionError(
                "Granola API authentication failed. Token may be expired. "
                "Try opening the Granola app to refresh your token."
            )

        response.raise_for_status()
        result = response.json()

        # Handle "Unsupported client" error (often means expired token)
        if isinstance(result, dict) and result.get("message") == "Unsupported client":
            if self.auto_refresh and _retry:
                # Try refreshing token and retry once
                self._access_token = None
                if self._refresh_token():
                    return self._make_request(endpoint, payload, _retry=False)
            raise PermissionError(
                "Granola API returned 'Unsupported client'. Token may be expired. "
                "Try opening the Granola app to refresh your token."
            )

        return result

    def _format_date(self, iso_date: Optional[str]) -> str:
        """Format ISO date to human-readable string."""
        if not iso_date:
            return "Unknown date"

        try:
            dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
            return dt.strftime("%b %d, %Y %I:%M %p")
        except (ValueError, AttributeError):
            return iso_date

    def _format_time(self, iso_date: Optional[str]) -> str:
        """Format timestamp to time-only string."""
        if not iso_date:
            return ""

        try:
            dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
            return dt.strftime("%H:%M:%S")
        except (ValueError, AttributeError):
            return ""

    def _extract_attendees(self, doc: dict) -> list[str]:
        """Extract attendee names from a document."""
        attendees = []

        # Try people dict
        people = doc.get("people") or {}
        if isinstance(people, dict):
            for attendee in people.get("attendees", []):
                if isinstance(attendee, dict):
                    details = attendee.get("details", {})
                    person = details.get("person", {})
                    name = person.get("name", {})
                    full_name = name.get("fullName") or attendee.get("email", "")
                    if full_name and full_name not in attendees:
                        attendees.append(full_name)

        # Also check Google Calendar event
        cal_attendees = (doc.get("google_calendar_event") or {}).get("attendees") or []
        for attendee in cal_attendees:
            name = attendee.get("displayName") or attendee.get("email", "")
            if name and name not in attendees:
                attendees.append(name)

        return attendees

    def _extract_notes_text(self, notes: dict) -> str:
        """Extract plain text from notes structure."""
        if not notes:
            return ""

        # notes_plain is available directly
        if isinstance(notes, str):
            return notes

        # Try to extract from content structure
        def extract_text(content):
            if not content:
                return ""
            if isinstance(content, str):
                return content
            if isinstance(content, list):
                return " ".join(extract_text(item) for item in content)
            if isinstance(content, dict):
                if content.get("type") == "text":
                    return content.get("text", "")
                texts = []
                if content.get("content"):
                    texts.append(extract_text(content["content"]))
                if content.get("text"):
                    texts.append(content["text"])
                return " ".join(texts)
            return ""

        return extract_text(notes)

    def get_documents(self, force_refresh: bool = False) -> list[dict]:
        """
        Get all documents from Granola API.

        Args:
            force_refresh: If True, bypass cache and fetch fresh data

        Returns:
            List of document dicts
        """
        if self._documents_cache is None or force_refresh:
            response = self._make_request("get-documents")
            # Handle API errors or unexpected response formats
            if isinstance(response, dict):
                if "message" in response:
                    print(f"Granola API error: {response['message']}")
                    self._documents_cache = []
                elif "documents" in response:
                    self._documents_cache = response["documents"]
                else:
                    self._documents_cache = []
            elif isinstance(response, list):
                self._documents_cache = response
            else:
                self._documents_cache = []
        return self._documents_cache

    def get_recent_meetings(self, limit: int = 5) -> list[MeetingSummary]:
        """
        Get recent meetings sorted by date.

        Args:
            limit: Maximum number of meetings to return (default: 5, max: 50)

        Returns:
            List of MeetingSummary objects, most recent first
        """
        limit = min(max(1, limit), 50)

        documents = self.get_documents()

        # Sort by date descending
        sorted_docs = sorted(
            documents,
            key=lambda d: d.get("created_at") or d.get("startTime") or "",
            reverse=True
        )

        results = []
        for doc in sorted_docs[:limit]:
            notes_text = doc.get("notes_plain", "") or self._extract_notes_text(doc.get("notes"))
            notes_summary = None
            if notes_text:
                notes_summary = notes_text[:200] + "..." if len(notes_text) > 200 else notes_text

            results.append(MeetingSummary(
                id=doc.get("id", ""),
                title=doc.get("title") or "Untitled Meeting",
                date=doc.get("created_at") or doc.get("startTime") or "",
                date_formatted=self._format_date(doc.get("created_at") or doc.get("startTime")),
                attendees=self._extract_attendees(doc),
                has_transcript=True,  # API always has transcripts available
                has_notes=bool(notes_text),
                notes_summary=notes_summary,
            ))

        return results

    def search_meetings(
        self,
        query: str,
        search_in: Literal["title", "attendees", "notes", "all"] = "all"
    ) -> list[MeetingSearchResult]:
        """
        Search meetings by query.

        Args:
            query: Search term
            search_in: Where to search - "title", "attendees", "notes", or "all"

        Returns:
            List of MeetingSearchResult objects sorted by relevance
        """
        documents = self.get_documents()
        query_lower = query.lower()

        results = []

        for doc in documents:
            matched_in = []
            relevance_score = 0

            # Search in title
            if search_in in ("all", "title"):
                title = (doc.get("title") or "").lower()
                if query_lower in title:
                    matched_in.append("title")
                    relevance_score += 10

            # Search in attendees
            if search_in in ("all", "attendees"):
                attendees = self._extract_attendees(doc)
                for attendee in attendees:
                    if query_lower in attendee.lower():
                        if "attendees" not in matched_in:
                            matched_in.append("attendees")
                            relevance_score += 5

            # Search in notes
            if search_in in ("all", "notes"):
                notes_text = (doc.get("notes_plain", "") or self._extract_notes_text(doc.get("notes"))).lower()
                if query_lower in notes_text:
                    matched_in.append("notes")
                    relevance_score += 3

            if matched_in:
                notes_text = doc.get("notes_plain", "") or self._extract_notes_text(doc.get("notes"))
                notes_summary = None
                if notes_text:
                    notes_summary = notes_text[:200] + "..." if len(notes_text) > 200 else notes_text

                results.append(MeetingSearchResult(
                    id=doc.get("id", ""),
                    title=doc.get("title") or "Untitled Meeting",
                    date=doc.get("created_at") or doc.get("startTime") or "",
                    date_formatted=self._format_date(doc.get("created_at") or doc.get("startTime")),
                    attendees=self._extract_attendees(doc),
                    has_transcript=True,  # API always has transcripts
                    has_notes=bool(notes_text),
                    notes_summary=notes_summary,
                    matched_in=matched_in,
                    relevance_score=relevance_score,
                ))

        # Sort by relevance
        results.sort(key=lambda r: (-r.relevance_score, r.date))

        return results

    def get_transcript(self, meeting_id: str) -> list[TranscriptEntry]:
        """
        Get transcript for a meeting.

        Args:
            meeting_id: The meeting UUID

        Returns:
            List of TranscriptEntry objects

        Raises:
            ValueError: If meeting or transcript not found
        """
        data = self._make_request("get-document-transcript", {"document_id": meeting_id})

        if not data:
            raise ValueError(f"No transcript found for meeting: {meeting_id}")

        # Sort by timestamp
        sorted_entries = sorted(data, key=lambda e: e.get("start_timestamp", ""))

        # Convert to TranscriptEntry objects
        entries = []
        for entry in sorted_entries:
            speaker = "You" if entry.get("source") == "microphone" else "Other"
            entries.append(TranscriptEntry(
                timestamp=self._format_time(entry.get("start_timestamp")),
                speaker=speaker,
                text=entry.get("text", ""),
            ))

        return entries

    def get_meeting(self, meeting_id: str) -> MeetingSummary:
        """
        Get a single meeting by ID.

        Args:
            meeting_id: The meeting UUID

        Returns:
            MeetingSummary object

        Raises:
            ValueError: If meeting not found
        """
        documents = self.get_documents()

        for doc in documents:
            if doc.get("id") == meeting_id:
                notes_text = doc.get("notes_plain", "") or self._extract_notes_text(doc.get("notes"))
                notes_summary = None
                if notes_text:
                    notes_summary = notes_text[:200] + "..." if len(notes_text) > 200 else notes_text

                return MeetingSummary(
                    id=doc.get("id", ""),
                    title=doc.get("title") or "Untitled Meeting",
                    date=doc.get("created_at") or doc.get("startTime") or "",
                    date_formatted=self._format_date(doc.get("created_at") or doc.get("startTime")),
                    attendees=self._extract_attendees(doc),
                    has_transcript=True,
                    has_notes=bool(notes_text),
                    notes_summary=notes_summary,
                )

        raise ValueError(f"Meeting not found: {meeting_id}")


# =============================================================================
# Wrapper Functions (for MCP tools)
# =============================================================================

def api_search_meetings(
    query: str,
    search_in: Literal["title", "attendees", "notes", "all"] = "all"
) -> dict:
    """
    Search for Granola meetings by title, attendee name, or content in notes.

    Uses the HTTP API for more reliable results.

    Args:
        query: Search query - can be a person's name, meeting title, or keyword from notes
        search_in: Where to search - "title", "attendees", "notes", or "all" (default: "all")

    Returns:
        Dict with matching meetings sorted by relevance
    """
    client = GranolaAPIClient()

    try:
        results = client.search_meetings(query=query, search_in=search_in)

        return {
            "success": True,
            "query": query,
            "search_in": search_in,
            "count": len(results),
            "meetings": [
                {
                    "id": m.id,
                    "title": m.title,
                    "date": m.date,
                    "date_formatted": m.date_formatted,
                    "attendees": m.attendees,
                    "has_transcript": m.has_transcript,
                    "has_notes": m.has_notes,
                    "notes_summary": m.notes_summary,
                    "matched_in": m.matched_in,
                    "relevance_score": m.relevance_score,
                }
                for m in results
            ],
        }
    except FileNotFoundError as e:
        return {
            "success": False,
            "error": str(e),
            "query": query,
            "meetings": [],
        }
    except PermissionError as e:
        return {
            "success": False,
            "error": str(e),
            "query": query,
            "meetings": [],
        }
    except Exception as e:
        return {
            "success": False,
            "error": f"Search failed: {e}",
            "query": query,
            "meetings": [],
        }


def api_get_transcript(meeting_id: str) -> dict:
    """
    Get the full transcript of a specific Granola meeting via API.

    Unlike the cache-based get_transcript, this fetches directly from
    Granola's backend, ensuring the transcript is available.

    Args:
        meeting_id: The unique ID of the meeting (UUID format)

    Returns:
        Dict with meeting metadata and transcript entries
    """
    client = GranolaAPIClient()

    try:
        entries = client.get_transcript(meeting_id=meeting_id)

        # Get meeting info for title/date
        try:
            meeting = client.get_meeting(meeting_id)
            meeting_title = meeting.title
            meeting_date = meeting.date_formatted
        except ValueError:
            meeting_title = "Unknown Meeting"
            meeting_date = ""

        return {
            "success": True,
            "meeting_id": meeting_id,
            "meeting_title": meeting_title,
            "meeting_date": meeting_date,
            "total_entries": len(entries),
            "transcript": [
                {
                    "timestamp": e.timestamp,
                    "speaker": e.speaker,
                    "text": e.text,
                }
                for e in entries
            ],
        }
    except FileNotFoundError as e:
        return {
            "success": False,
            "error": str(e),
            "meeting_id": meeting_id,
        }
    except PermissionError as e:
        return {
            "success": False,
            "error": str(e),
            "meeting_id": meeting_id,
        }
    except ValueError as e:
        return {
            "success": False,
            "error": str(e),
            "meeting_id": meeting_id,
        }
    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to get transcript: {e}",
            "meeting_id": meeting_id,
        }
