"""
Fansly Direct API Client

Downloads content directly from the Fansly API (not via Coomer).
"""

import aiohttp
import asyncio
from datetime import datetime
from typing import List, Optional, Dict, Any, Callable

from modules.base_module import LoggingMixin, RateLimitMixin
from .models import Post, Attachment, Message


class FanslyDirectClient(LoggingMixin, RateLimitMixin):
    """
    API client for downloading content directly from Fansly

    API Endpoints:
    - Base URL: https://apiv3.fansly.com/api/v1
    - Auth: Authorization header with token
    - GET /account?usernames={username} - Get account info
    - GET /timelinenew/{account_id}?before={cursor} - Get posts (paginated)
    """

    BASE_URL = "https://apiv3.fansly.com/api/v1"
    SERVICE_ID = "fansly_direct"
    PLATFORM = "fansly"

    def __init__(self, auth_token: str, log_callback: Optional[Callable] = None):
        self._init_logger('PaidContent', log_callback, default_module='FanslyDirect')
        # Conservative rate limiting for real Fansly API
        self._init_rate_limiter(min_delay=1.0, max_delay=3.0, batch_delay_min=2, batch_delay_max=5)

        self.auth_token = auth_token
        self._session: Optional[aiohttp.ClientSession] = None

    async def _get_session(self) -> aiohttp.ClientSession:
        """Get or create aiohttp session with Fansly headers"""
        if self._session is None or self._session.closed:
            headers = {
                'Authorization': self.auth_token,
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
                'Accept': 'application/json, text/plain, */*',
                'Accept-Language': 'en-US,en;q=0.9',
                'Origin': 'https://fansly.com',
                'Referer': 'https://fansly.com/',
            }
            timeout = aiohttp.ClientTimeout(total=60)
            self._session = aiohttp.ClientSession(headers=headers, timeout=timeout)
        return self._session

    async def close(self):
        """Close the aiohttp session"""
        if self._session and not self._session.closed:
            await self._session.close()
            self._session = None

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()

    async def check_auth(self) -> Dict[str, Any]:
        """
        Verify the auth token is valid.

        Returns:
            Dict with 'valid' bool and optionally 'username' and 'account_id'
        """
        self._delay_between_items()
        try:
            session = await self._get_session()

            # Get own account info to verify auth
            async with session.get(f"{self.BASE_URL}/account/me") as resp:
                if resp.status == 200:
                    data = await resp.json()
                    account = data.get('response', {}).get('account', {})
                    return {
                        'valid': True,
                        'account_id': account.get('id'),
                        'username': account.get('username'),
                        'display_name': account.get('displayName'),
                    }
                elif resp.status == 401:
                    return {'valid': False, 'error': 'Invalid or expired auth token'}
                else:
                    return {'valid': False, 'error': f'HTTP {resp.status}'}

        except Exception as e:
            self.log(f"Error checking auth: {e}", 'error')
            return {'valid': False, 'error': str(e)}

    async def get_account_info(self, username: str) -> Optional[Dict[str, Any]]:
        """
        Get account info for a username.

        Args:
            username: The Fansly username

        Returns:
            Account info dict or None if not found
        """
        self._delay_between_items()
        try:
            session = await self._get_session()

            async with session.get(f"{self.BASE_URL}/account", params={'usernames': username}) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    accounts = data.get('response', [])
                    if accounts and len(accounts) > 0:
                        account = accounts[0]
                        # Extract bio from various possible field names
                        bio = None
                        for field in ['about', 'bio', 'description', 'rawAbout', 'profileDescription']:
                            if account.get(field):
                                bio = account.get(field)
                                self.log(f"Found bio in field '{field}': {bio[:50] if len(bio) > 50 else bio}...", 'debug')
                                break

                        # Extract location
                        location = account.get('location') or None

                        # Extract social links (profileSocials)
                        external_links = None
                        profile_socials = account.get('profileSocials', [])
                        if profile_socials:
                            import json
                            external_links = json.dumps(profile_socials)

                        # Log available fields for debugging
                        self.log(f"Account fields: {list(account.keys())}", 'debug')

                        return {
                            'account_id': account.get('id'),
                            'username': account.get('username'),
                            'display_name': account.get('displayName'),
                            'avatar_url': self._get_avatar_url(account),
                            'banner_url': self._get_banner_url(account),
                            'bio': bio,
                            'location': location,
                            'external_links': external_links,
                        }
                    self.log(f"Account not found: {username}", 'warning')
                    return None
                elif resp.status == 404:
                    self.log(f"Account not found: {username}", 'warning')
                    return None
                else:
                    self.log(f"Error getting account info: HTTP {resp.status}", 'warning')
                    return None

        except Exception as e:
            self.log(f"Error getting account info for {username}: {e}", 'error')
            return None

    def _get_avatar_url(self, account: Dict) -> Optional[str]:
        """Extract avatar URL from account data"""
        avatar = account.get('avatar')
        if avatar and avatar.get('locations'):
            locations = avatar.get('locations', [])
            if locations:
                return locations[0].get('location')
        return None

    def _get_banner_url(self, account: Dict) -> Optional[str]:
        """Extract banner URL from account data"""
        banner = account.get('banner')
        if banner and banner.get('locations'):
            locations = banner.get('locations', [])
            if locations:
                return locations[0].get('location')
        return None

    async def get_single_post(self, post_id: str, account_id: str) -> Optional['Post']:
        """
        Fetch a single post by its Fansly post ID.
        Uses the /post endpoint to get one specific post with its media.

        Args:
            post_id: The Fansly post ID
            account_id: The account ID of the creator (for parsing)

        Returns:
            Post object or None
        """
        self._delay_between_items()
        try:
            session = await self._get_session()

            params = {'ngsw-bypass': 'true', 'ids': post_id}
            url = f"{self.BASE_URL}/post"
            async with session.get(url, params=params) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    response = data.get('response', {})

                    posts = response.get('posts', [])
                    if not posts:
                        self.log(f"Post {post_id} not found in API response", 'warning')
                        return None

                    # Build lookup dicts same as timeline
                    media_dict = {}
                    for m in response.get('media', []):
                        media_dict[m.get('id')] = m

                    account_media_dict = {}
                    for am in response.get('accountMedia', []):
                        account_media_dict[am.get('id')] = am

                    bundle_dict = {}
                    for bundle in response.get('accountMediaBundles', []):
                        bundle_dict[bundle.get('id')] = bundle

                    polls_dict = {}
                    for poll in response.get('polls', []):
                        polls_dict[poll.get('id')] = poll

                    # Build minimal account dict for parsing
                    account = {'account_id': account_id}
                    # Try to get account info from response
                    accounts = response.get('accounts', [])
                    if accounts:
                        acc = accounts[0]
                        account['account_id'] = str(acc.get('id', account_id))

                    post = self._parse_post(posts[0], account, media_dict, account_media_dict, bundle_dict, polls_dict)
                    return post
                else:
                    self.log(f"Error fetching post {post_id}: HTTP {resp.status}", 'warning')
                    return None

        except Exception as e:
            self.log(f"Error fetching single post {post_id}: {e}", 'error')
            return None

    async def get_posts(
        self,
        username: str,
        since_date: Optional[str] = None,
        until_date: Optional[str] = None,
        days_back: Optional[int] = None,
        max_posts: Optional[int] = None,
        progress_callback: Optional[Callable[[int, int], None]] = None
    ) -> List[Post]:
        """
        Fetch posts from a creator's timeline.

        Args:
            username: The Fansly username
            since_date: Only fetch posts after this date (ISO format)
            until_date: Only fetch posts before this date (ISO format)
            days_back: Fetch posts from the last N days
            max_posts: Maximum number of posts to fetch
            progress_callback: Called with (page, total_posts) during fetching

        Returns:
            List of Post objects
        """
        # First get account info to get the account ID
        account = await self.get_account_info(username)
        if not account:
            self.log(f"Could not find account for {username}", 'error')
            return []

        account_id = account['account_id']
        self.log(f"Fetching posts for {username} (account_id: {account_id})", 'info')

        # Calculate date filters
        since_timestamp = None
        until_timestamp = None

        if days_back:
            from datetime import timedelta
            since_date = (datetime.now() - timedelta(days=days_back)).isoformat()

        if since_date:
            try:
                since_dt = datetime.fromisoformat(since_date.replace('Z', '+00:00'))
                since_timestamp = int(since_dt.timestamp())  # Fansly uses seconds
            except (ValueError, TypeError):
                pass

        if until_date:
            try:
                until_dt = datetime.fromisoformat(until_date.replace('Z', '+00:00'))
                until_timestamp = int(until_dt.timestamp())  # Fansly uses seconds
            except (ValueError, TypeError):
                pass

        # Fetch posts with pagination
        all_posts: List[Post] = []
        cursor: Optional[str] = None
        page = 0
        all_pinned_posts: Dict[str, Dict] = {}

        while True:
            posts_batch, new_cursor, media_dict, account_media_dict, bundle_dict, pinned_posts_dict, polls_dict = await self._fetch_timeline_page(
                account_id, cursor
            )

            # Collect pinned posts from all pages (they're in every response)
            all_pinned_posts.update(pinned_posts_dict)

            if not posts_batch:
                break

            for post_data in posts_batch:
                post = self._parse_post(post_data, account, media_dict, account_media_dict, bundle_dict, polls_dict)
                if not post:
                    continue

                # Check if this post is pinned
                post_id_str = str(post_data.get('id', ''))
                if post_id_str in all_pinned_posts:
                    post.is_pinned = True
                    post.pinned_at = all_pinned_posts[post_id_str].get('pinned_at')
                    self.log(f"Post {post_id_str} is pinned (pinned at {post.pinned_at})", 'debug')

                # Check date filters
                post_timestamp = post_data.get('createdAt', 0)

                # Stop if we've gone past the since_date
                if since_timestamp and post_timestamp < since_timestamp:
                    self.log(f"Reached posts older than since_date, stopping", 'debug')
                    return all_posts

                # Skip posts newer than until_date
                if until_timestamp and post_timestamp > until_timestamp:
                    continue

                all_posts.append(post)

                if max_posts and len(all_posts) >= max_posts:
                    self.log(f"Reached max_posts limit: {max_posts}", 'debug')
                    return all_posts

            page += 1
            if progress_callback:
                progress_callback(page, len(all_posts))

            # Check if we have more pages
            if not new_cursor:
                break

            cursor = new_cursor
            self._delay_between_batches()

        # Store pinned posts info for the caller to update DB for posts outside date range
        self._last_pinned_posts = all_pinned_posts

        self.log(f"Fetched {len(all_posts)} posts for {username}", 'info')
        return all_posts

    async def _fetch_timeline_page(
        self,
        account_id: str,
        cursor: Optional[str] = None
    ) -> tuple[List[Dict], Optional[str], Dict, Dict, Dict, Dict, Dict]:
        """
        Fetch a single page of timeline posts.

        Returns:
            Tuple of (posts, next_cursor, media_dict, account_media_dict, bundle_dict, pinned_posts_dict, polls_dict)
        """
        self._delay_between_items()
        try:
            session = await self._get_session()

            params = {'ngsw-bypass': 'true'}
            if cursor:
                params['before'] = cursor

            url = f"{self.BASE_URL}/timelinenew/{account_id}"
            async with session.get(url, params=params) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    response = data.get('response', {})

                    posts = response.get('posts', [])
                    # Build media lookup dicts
                    media_dict = {}
                    for m in response.get('media', []):
                        media_dict[m.get('id')] = m

                    account_media_dict = {}
                    for am in response.get('accountMedia', []):
                        account_media_dict[am.get('id')] = am

                    # Build bundle lookup dict (for contentType=2 attachments)
                    bundle_dict = {}
                    for bundle in response.get('accountMediaBundles', []):
                        bundle_dict[bundle.get('id')] = bundle

                    # Build polls lookup dict (for contentType=42001 attachments)
                    polls_dict = {}
                    for poll in response.get('polls', []):
                        polls_dict[poll.get('id')] = poll

                    # Extract pinned posts from accounts
                    pinned_posts_dict = {}
                    for acc in response.get('accounts', []):
                        for pinned in acc.get('pinnedPosts', []):
                            post_id = pinned.get('postId')
                            if post_id:
                                # createdAt is in milliseconds
                                pinned_at = pinned.get('createdAt', 0)
                                if pinned_at > 1000000000000:
                                    pinned_at = pinned_at / 1000
                                pinned_posts_dict[str(post_id)] = {
                                    'pinned_at': datetime.fromtimestamp(pinned_at).isoformat() if pinned_at else None,
                                    'pos': pinned.get('pos', 0)
                                }

                    # Get cursor for next page (id of last post)
                    next_cursor = None
                    if posts:
                        last_post = posts[-1]
                        next_cursor = str(last_post.get('id'))

                    return posts, next_cursor, media_dict, account_media_dict, bundle_dict, pinned_posts_dict, polls_dict

                elif resp.status == 401:
                    self.log("Auth token expired or invalid", 'error')
                    return [], None, {}, {}, {}, {}, {}
                else:
                    self.log(f"Error fetching timeline: HTTP {resp.status}", 'warning')
                    return [], None, {}, {}, {}, {}, {}

        except Exception as e:
            self.log(f"Error fetching timeline page: {e}", 'error')
            return [], None, {}, {}, {}, {}, {}

    def _parse_post(
        self,
        post_data: Dict,
        account: Dict,
        media_dict: Dict,
        account_media_dict: Dict,
        bundle_dict: Dict,
        polls_dict: Optional[Dict] = None
    ) -> Optional[Post]:
        """
        Parse Fansly post data into a Post model.

        Args:
            post_data: Raw post data from API
            account: Account info dict
            media_dict: Media lookup dict
            account_media_dict: Account media lookup dict
            bundle_dict: Media bundle lookup dict (for contentType=2)
            polls_dict: Poll lookup dict (for contentType=42001)

        Returns:
            Post object or None if parsing fails
        """
        try:
            post_id = str(post_data.get('id', ''))
            if not post_id:
                return None

            # Parse timestamp
            created_at = post_data.get('createdAt', 0)
            published_at = None
            if created_at:
                try:
                    # Fansly uses seconds
                    published_at = datetime.fromtimestamp(created_at).isoformat()
                except (ValueError, TypeError, OSError):
                    pass

            # Get content/description
            content = post_data.get('content', '')

            # Parse attachments
            # Attachments are objects with contentId that maps to accountMedia or bundles
            # contentType=1: single media in accountMedia
            # contentType=2: media bundle in accountMediaBundles
            # contentType=42001: poll (looked up in polls_dict)
            attachments = []
            poll_texts = []
            attachment_list = post_data.get('attachments', [])

            for att_obj in attachment_list:
                # att_obj is a dict like: {"postId": "...", "pos": 0, "contentType": 1, "contentId": "..."}
                if not isinstance(att_obj, dict):
                    continue
                content_id = att_obj.get('contentId')
                content_type = att_obj.get('contentType', 1)
                if not content_id:
                    continue

                # Handle polls (contentType 42001)
                if content_type == 42001:
                    if polls_dict:
                        poll = polls_dict.get(content_id)
                        if poll:
                            poll_text = self._format_poll(poll)
                            if poll_text:
                                poll_texts.append(poll_text)
                    continue

                # Only process known media content types
                # contentType 1 = single media
                # contentType 2 = media bundle
                if content_type not in (1, 2):
                    continue

                if content_type == 2:
                    # Bundle: look up in bundle_dict, then expand to multiple media items
                    bundle = bundle_dict.get(content_id, {})
                    account_media_ids = bundle.get('accountMediaIds', [])
                    for am_id in account_media_ids:
                        account_media = account_media_dict.get(am_id, {})
                        media = account_media.get('media', {})
                        attachment = self._parse_attachment(account_media, media)
                        if attachment:
                            attachments.append(attachment)
                else:
                    # Single media: look up directly in accountMedia
                    account_media = account_media_dict.get(content_id, {})
                    media = account_media.get('media', {})
                    attachment = self._parse_attachment(account_media, media)
                    if attachment:
                        attachments.append(attachment)

            # Append poll text to content if any polls were found
            if poll_texts:
                poll_section = '\n'.join(poll_texts)
                if content:
                    content = content + '\n\n' + poll_section
                else:
                    content = poll_section

            return Post(
                post_id=post_id,
                service_id=self.SERVICE_ID,
                platform=self.PLATFORM,
                creator_id=account.get('account_id', ''),
                title=None,  # Fansly posts don't have titles
                content=content,
                published_at=published_at,
                added_at=datetime.now().isoformat(),
                attachments=attachments,
            )

        except Exception as e:
            self.log(f"Error parsing post: {e}", 'error')
            return None

    def _format_poll(self, poll: Dict) -> Optional[str]:
        """Format a Fansly poll into readable text for post content."""
        try:
            # Fansly polls use 'title' for the question, options use 'title' and 'voteCount'
            question = (poll.get('title') or poll.get('question') or '').strip()
            options = poll.get('options', [])
            if not options:
                return None

            lines = []
            if question:
                lines.append(f"[Poll] {question}")
            else:
                lines.append("[Poll]")

            total_votes = sum(opt.get('voteCount', 0) or opt.get('votes', 0) for opt in options)
            for opt in options:
                text = (opt.get('title') or opt.get('text') or '').strip()
                votes = opt.get('voteCount', 0) or opt.get('votes', 0)
                if total_votes > 0:
                    pct = round(votes / total_votes * 100)
                    lines.append(f"  - {text} ({pct}%, {votes} votes)")
                else:
                    lines.append(f"  - {text}")

            if total_votes > 0:
                lines.append(f"  Total: {total_votes} votes")

            return '\n'.join(lines)
        except Exception:
            return None

    def _parse_attachment(self, account_media: Dict, media: Dict) -> Optional[Attachment]:
        """
        Parse Fansly media data into an Attachment model.

        Args:
            account_media: Account media data (links post to media)
            media: Media details (embedded in account_media)

        Returns:
            Attachment object or None if parsing fails
        """
        try:
            if not media:
                return None

            media_id = str(media.get('id', ''))
            mimetype = media.get('mimetype', '')

            # Determine file type
            file_type = 'unknown'
            if mimetype.startswith('image/'):
                file_type = 'image'
            elif mimetype.startswith('video/'):
                file_type = 'video'
            elif mimetype.startswith('audio/'):
                file_type = 'audio'

            # Get dimensions and metadata from main media object
            width = media.get('width')
            height = media.get('height')
            file_size = None
            duration = None

            # Parse metadata JSON for duration and original dimensions
            metadata_str = media.get('metadata', '{}')
            try:
                import json
                metadata = json.loads(metadata_str) if metadata_str else {}
                duration = metadata.get('duration')
                # Use original dimensions if available (higher quality)
                if metadata.get('originalWidth'):
                    width = metadata.get('originalWidth')
                if metadata.get('originalHeight'):
                    height = metadata.get('originalHeight')
            except (json.JSONDecodeError, TypeError):
                pass

            # Get signed download URL
            # Priority: 1) Direct video URL (MP4/MOV) from main locations
            #           2) Direct video URL from variants
            #           3) Streaming manifest (m3u8/mpd) as fallback
            download_url = None

            # Extensions for different types
            streaming_exts = ('.m3u8', '.mpd')
            image_exts = ('.jpeg', '.jpg', '.png', '.gif', '.webp')
            video_exts = ('.mp4', '.mov', '.webm', '.mkv', '.avi')

            def url_ext(url: str) -> str:
                """Get extension from URL, ignoring query string"""
                path = url.split('?')[0] if url else ''
                return '.' + path.split('.')[-1].lower() if '.' in path else ''

            # Check for quality options
            main_locations = media.get('locations', [])
            variants = media.get('variants', [])
            self.log(f"Media {media_id}: {len(main_locations)} main URLs, {len(variants)} variants", 'debug')

            if variants:
                # Debug: log available variants at debug level
                for v in variants:
                    v_w, v_h = v.get('width', 0), v.get('height', 0)
                    v_locs = v.get('locations', [])
                    for loc in v_locs:
                        loc_url = loc.get('location', '')
                        ext = url_ext(loc_url)
                        self.log(f"  Variant {v_w}x{v_h}: ext={ext}", 'debug')

                # Sort by resolution (highest first)
                sorted_variants = sorted(
                    variants,
                    key=lambda v: (v.get('width', 0) or 0) * (v.get('height', 0) or 0),
                    reverse=True
                )

                # For videos: PRIORITIZE highest resolution, even if streaming
                # 1) First look for highest res direct video in variants
                # 2) Then highest res streaming (m3u8/mpd) - this is often 4K
                # 3) Fall back to main locations (often lower res direct)
                if file_type == 'video':
                    # Check for direct video at highest res
                    for variant in sorted_variants:
                        var_locations = variant.get('locations', [])
                        for loc in var_locations:
                            url = loc.get('location', '')
                            ext = url_ext(url)
                            if url and ext in video_exts:
                                download_url = url
                                var_width = variant.get('width')
                                var_height = variant.get('height')
                                if var_width and var_height:
                                    self.log(f"Selected: {var_width}x{var_height} direct video from variants", 'info')
                                    width = var_width
                                    height = var_height
                                break
                        if download_url:
                            break

                    # If no direct video, use streaming at highest res (4K usually)
                    if not download_url:
                        for variant in sorted_variants:
                            var_locations = variant.get('locations', [])
                            for loc in var_locations:
                                url = loc.get('location', '')
                                ext = url_ext(url)
                                if url and ext in streaming_exts:
                                    # Get CloudFront signed URL params from metadata
                                    loc_metadata = loc.get('metadata', {})
                                    if loc_metadata:
                                        # Append signed URL params: Key-Pair-Id, Signature, Policy
                                        params = []
                                        for key in ['Key-Pair-Id', 'Signature', 'Policy']:
                                            if key in loc_metadata:
                                                params.append(f"{key}={loc_metadata[key]}")
                                        if params:
                                            separator = '&' if '?' in url else '?'
                                            url = url + separator + '&'.join(params)
                                            self.log(f"Constructed signed streaming URL with {len(params)} params", 'info')

                                    download_url = url
                                    var_width = variant.get('width')
                                    var_height = variant.get('height')
                                    stream_type = 'HLS' if ext == '.m3u8' else 'DASH'
                                    if var_width and var_height:
                                        self.log(f"Selected: {var_width}x{var_height} {stream_type} stream (highest quality)", 'info')
                                        width = var_width
                                        height = var_height
                                    break
                            if download_url:
                                break

                    # Fall back to main locations (often 720p direct)
                    if not download_url:
                        for loc in main_locations:
                            url = loc.get('location', '')
                            ext = url_ext(url)
                            if url and ext in video_exts:
                                download_url = url
                                self.log(f"Fallback: direct video from main locations (lower res)", 'info')
                                break

                # For images: prefer main locations (original full resolution)
                # Variants are scaled-down thumbnails (1080p max)
                if file_type == 'image' and not download_url:
                    # First check main locations for original resolution
                    for loc in main_locations:
                        url = loc.get('location', '')
                        ext = url_ext(url)
                        if url and ext in image_exts:
                            download_url = url
                            # Use dimensions from media object (original size)
                            orig_width = media.get('width')
                            orig_height = media.get('height')
                            if orig_width and orig_height:
                                self.log(f"Selected: {orig_width}x{orig_height} original image", 'info')
                                width = orig_width
                                height = orig_height
                            break

                    # Fallback to variants if main locations don't have image
                    if not download_url:
                        for variant in sorted_variants:
                            var_locations = variant.get('locations', [])
                            for loc in var_locations:
                                url = loc.get('location', '')
                                ext = url_ext(url)
                                if url and ext in image_exts:
                                    download_url = url
                                    var_width = variant.get('width')
                                    var_height = variant.get('height')
                                    if var_width and var_height:
                                        self.log(f"Selected: {var_width}x{var_height} image (variant)", 'info')
                                        width = var_width
                                        height = var_height
                                    break
                            if download_url:
                                break

            # Final fallback: any URL from main locations
            if not download_url and main_locations:
                for loc in main_locations:
                    url = loc.get('location', '')
                    if url:
                        download_url = url
                        self.log(f"Final fallback: {url[:80]}...", 'info')
                        break

            # Fallback: relative path (won't work for download but useful for reference)
            location_path = media.get('location', '')

            # Extract filename and extension first (needed for placeholder too)
            ext = ''
            if '/' in mimetype:
                ext = mimetype.split('/')[-1]
                if ext == 'jpeg':
                    ext = 'jpg'
                elif ext == 'quicktime':
                    ext = 'mov'

            filename = f"{media_id}.{ext}" if ext else media_id

            # Log if this is PPV content (no download URL available)
            if not download_url:
                self.log(f"PPV/locked content detected: {filename} - creating placeholder for manual import", 'info')

            # Flag videos below 4K for quality recheck (Fansly may still be processing)
            needs_recheck = False
            if file_type == 'video' and download_url and width and height:
                is_4k = (width >= 3840 and height >= 2160) or (width >= 2160 and height >= 3840)
                if not is_4k:
                    needs_recheck = True

            return Attachment(
                name=filename,
                server_path=location_path,  # Relative path for reference
                file_type=file_type,
                extension=ext if ext else None,
                download_url=download_url,  # None for PPV content - will need manual import
                file_size=file_size,
                width=width,
                height=height,
                duration=duration,
                needs_quality_recheck=needs_recheck,
            )

        except Exception as e:
            self.log(f"Error parsing attachment: {e}", 'error')
            return None

    # ==================== MESSAGES ====================

    async def get_chat_list(self) -> List[Dict]:
        """
        Get list of messaging conversations.

        Uses GET /messaging/groups to list all chat groups.

        Returns:
            List of dicts with 'group_id' and 'partner_account_id' keys
        """
        self._delay_between_items()
        try:
            session = await self._get_session()
            url = f"{self.BASE_URL}/messaging/groups"
            params = {'ngsw-bypass': 'true'}

            async with session.get(url, params=params) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    response = data.get('response', {})
                    groups = response.get('groups', [])

                    results = []
                    for group in groups:
                        group_id = group.get('id')
                        # partnerAccountId is the other user in the conversation
                        partner_ids = group.get('users', [])
                        partner_id = None
                        for uid in partner_ids:
                            # The partner is the one that's not us
                            if uid != group.get('createdBy'):
                                partner_id = uid
                                break
                        if not partner_id and partner_ids:
                            partner_id = partner_ids[0]

                        if group_id:
                            results.append({
                                'group_id': group_id,
                                'partner_account_id': partner_id,
                            })

                    self.log(f"Found {len(results)} chat groups", 'info')
                    return results

                elif resp.status == 401:
                    self.log("Auth token expired or invalid", 'error')
                    return []
                else:
                    self.log(f"Error fetching chat list: HTTP {resp.status}", 'warning')
                    return []

        except Exception as e:
            self.log(f"Error fetching chat list: {e}", 'error')
            return []

    async def get_messages(self, group_id: str, creator_account_id: str,
                           max_messages: int = 500) -> List[Message]:
        """
        Fetch messages from a conversation.

        Uses GET /message?groupId={id}&limit=50 with cursor-based pagination.

        Args:
            group_id: Fansly messaging group ID
            creator_account_id: Fansly account ID of the creator (to determine direction)
            max_messages: Maximum number of messages to fetch

        Returns:
            List of Message objects
        """
        messages = []
        cursor = None
        page = 0

        while len(messages) < max_messages:
            page += 1
            self._delay_between_items()

            try:
                session = await self._get_session()
                params = {
                    'groupId': group_id,
                    'limit': 50,
                    'ngsw-bypass': 'true',
                }
                if cursor:
                    params['before'] = cursor

                url = f"{self.BASE_URL}/message"
                async with session.get(url, params=params) as resp:
                    if resp.status != 200:
                        self.log(f"Error fetching messages: HTTP {resp.status}", 'warning')
                        break

                    data = await resp.json()
                    response = data.get('response', {})

                    msg_list = response.get('messages', [])
                    if not msg_list:
                        break

                    # Build media lookup dicts (same structure as timeline)
                    account_media_dict = {}
                    for am in response.get('accountMedia', []):
                        account_media_dict[am.get('id')] = am

                    bundle_dict = {}
                    for bundle in response.get('accountMediaBundles', []):
                        bundle_dict[bundle.get('id')] = bundle

                    for msg_data in msg_list:
                        msg = self._parse_fansly_message(
                            msg_data, creator_account_id,
                            account_media_dict, bundle_dict
                        )
                        if msg:
                            messages.append(msg)

                    self.log(f"Fetched page {page}: {len(msg_list)} messages (total: {len(messages)})", 'debug')

                    if len(msg_list) < 50:
                        break  # Last page

                    # Cursor for next page
                    last_msg = msg_list[-1]
                    next_cursor = str(last_msg.get('id', ''))
                    if next_cursor and next_cursor != str(cursor):
                        cursor = next_cursor
                    else:
                        break

            except Exception as e:
                self.log(f"Error fetching messages page {page}: {e}", 'error')
                break

        self.log(f"Fetched {len(messages)} messages for group {group_id}", 'info')
        return messages

    def _parse_fansly_message(self, msg_data: Dict, creator_account_id: str,
                               account_media_dict: Dict, bundle_dict: Dict) -> Optional[Message]:
        """
        Parse a Fansly message into a Message model.

        Args:
            msg_data: Raw message dict from API
            creator_account_id: Fansly account ID of the creator
            account_media_dict: Media lookup dict from response
            bundle_dict: Bundle lookup dict from response

        Returns:
            Message object or None
        """
        try:
            msg_id = str(msg_data.get('id', ''))
            if not msg_id:
                return None

            sender_id = str(msg_data.get('senderId', ''))
            is_from_creator = (sender_id == str(creator_account_id))

            text = msg_data.get('content', '')

            # Parse timestamp (Fansly uses seconds or milliseconds)
            created_at = msg_data.get('createdAt', 0)
            sent_at = None
            if created_at:
                try:
                    ts = created_at
                    if ts > 1000000000000:
                        ts = ts / 1000
                    sent_at = datetime.fromtimestamp(ts).isoformat()
                except (ValueError, TypeError, OSError):
                    pass

            # PPV/price info
            price = msg_data.get('price')
            is_tip = msg_data.get('isTip', False)
            tip_amount = msg_data.get('tipAmount')

            # Parse attachments - same pattern as posts
            attachments = []
            attachment_list = msg_data.get('attachments', []) or []
            for att_obj in attachment_list:
                if not isinstance(att_obj, dict):
                    continue
                content_id = att_obj.get('contentId')
                content_type = att_obj.get('contentType', 1)
                if not content_id:
                    continue
                if content_type not in (1, 2):
                    continue

                if content_type == 2:
                    bundle = bundle_dict.get(content_id, {})
                    for am_id in bundle.get('accountMediaIds', []):
                        account_media = account_media_dict.get(am_id, {})
                        media = account_media.get('media', {})
                        att = self._parse_attachment(account_media, media)
                        if att:
                            attachments.append(att)
                else:
                    account_media = account_media_dict.get(content_id, {})
                    media = account_media.get('media', {})
                    att = self._parse_attachment(account_media, media)
                    if att:
                        attachments.append(att)

            return Message(
                message_id=msg_id,
                platform=self.PLATFORM,
                service_id=self.SERVICE_ID,
                creator_id=str(creator_account_id),
                text=text if text else None,
                sent_at=sent_at,
                is_from_creator=is_from_creator,
                is_tip=bool(is_tip),
                tip_amount=float(tip_amount) if tip_amount else None,
                price=float(price) if price else None,
                is_free=price is None or price == 0,
                is_purchased=False,
                attachments=attachments,
            )

        except Exception as e:
            self.log(f"Error parsing fansly message: {e}", 'error')
            return None

    async def recheck_attachment_quality(self, attachment_id: int, db) -> Dict:
        """
        Recheck a single attachment for higher quality variants via the Fansly API.
        Returns: {'upgraded': bool, 'new_width': int, 'new_height': int, 'old_width': int, 'old_height': int, 'creator_id': int}
        """
        att = db.get_attachment(attachment_id)
        if not att:
            return {'upgraded': False, 'error': 'Attachment not found'}

        post = db.get_post(att['post_id'])
        if not post:
            return {'upgraded': False, 'error': 'Post not found'}

        creator = db.get_creator(post['creator_id'])
        if not creator:
            return {'upgraded': False, 'error': 'Creator not found'}

        account_id = creator.get('creator_id')
        post_id = post.get('post_id')
        old_width = att.get('width') or 0
        old_height = att.get('height') or 0
        media_id = att.get('name', '').replace('.mp4', '').replace('.mov', '').replace('.jpg', '').replace('.png', '')

        try:
            # Fetch timeline page containing this post
            posts, _, media_dict, account_media_dict, bundle_dict, _, _ = await self._fetch_timeline_page(
                account_id=account_id,
                cursor=str(int(post_id) + 1)
            )

            best_width = old_width
            best_height = old_height
            best_url = None

            # Search account_media_dict for matching media
            for am_id, am_data in account_media_dict.items():
                media = am_data.get('media', {})
                if str(media.get('id')) == media_id:
                    variants = media.get('variants', [])
                    for v in variants:
                        v_w = v.get('width', 0) or 0
                        v_h = v.get('height', 0) or 0
                        if v_w * v_h > best_width * best_height:
                            for loc in v.get('locations', []):
                                loc_url = loc.get('location', '')
                                if '.m3u8' in loc_url or '.mp4' in loc_url or '.mov' in loc_url:
                                    best_width = v_w
                                    best_height = v_h
                                    metadata = loc.get('metadata', {})
                                    if metadata:
                                        params = []
                                        for key in ['Key-Pair-Id', 'Signature', 'Policy']:
                                            if key in metadata:
                                                params.append(f"{key}={metadata[key]}")
                                        best_url = loc_url + ('?' + '&'.join(params) if params else '')
                                    else:
                                        best_url = loc_url
                                    break
                    break

            # Update quality check tracking
            now = datetime.now().isoformat()
            recheck_count = (att.get('quality_recheck_count') or 0) + 1

            if best_url and (best_width > old_width or best_height > old_height):
                # Upgrade found — update attachment
                with db.get_connection(for_write=True) as conn:
                    conn.execute("""
                        UPDATE paid_content_attachments
                        SET download_url = ?, width = ?, height = ?,
                            status = 'pending', download_attempts = 0, error_message = NULL,
                            local_path = NULL, local_filename = NULL, file_hash = NULL,
                            needs_quality_recheck = 0, last_quality_check = ?, quality_recheck_count = ?
                        WHERE id = ?
                    """, (best_url, best_width, best_height, now, recheck_count, attachment_id))
                    conn.commit()

                self.log(f"Quality upgrade: {att.get('name')} {old_width}x{old_height} -> {best_width}x{best_height}", 'info')
                return {
                    'upgraded': True,
                    'old_width': old_width, 'old_height': old_height,
                    'new_width': best_width, 'new_height': best_height,
                    'creator_id': post['creator_id']
                }
            else:
                # No upgrade found
                with db.get_connection(for_write=True) as conn:
                    conn.execute("""
                        UPDATE paid_content_attachments
                        SET last_quality_check = ?, quality_recheck_count = ?
                        WHERE id = ?
                    """, (now, recheck_count, attachment_id))
                    conn.commit()

                return {
                    'upgraded': False,
                    'old_width': old_width, 'old_height': old_height,
                    'creator_id': post['creator_id']
                }

        except Exception as e:
            self.log(f"Quality recheck error for attachment {attachment_id}: {e}", 'error')
            return {'upgraded': False, 'error': str(e)}
