import requests
import time
import base64
import hashlib
import secrets
import datetime
from urllib.parse import urlencode

from django.conf import settings
from django.utils import timezone
from apps.social.utils.x import XConfig
import logging
logger = logging.getLogger(__name__)

class XAPIError(Exception):
    pass

class XRateLimitError(Exception):
    """Custom exception to signal Celery to retry with a countdown."""
    def __init__(self, reset_at_timestamp):
        self.reset_at = reset_at_timestamp
        super().__init__(f"Rate limit hit. Retry after {reset_at_timestamp}")

class XService:

    TWITTER_EPOCH = 1288834974657

    # --- AUTHENTICATION ---

    @staticmethod
    def _get_auth_header():
        """
        Creates the Authorization header using Basic Auth.
        Required for Confidential Clients (Web Apps).
        """
        auth_str = f"{settings.X_CLIENT_ID}:{settings.X_CLIENT_SECRET}"
        encoded_auth = base64.b64encode(auth_str.encode()).decode()
        return {"Authorization": f"Basic {encoded_auth}", "Content-Type": "application/x-www-form-urlencoded"}

    @staticmethod
    def generate_pkce_pair():
        code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').replace('=', '')
        code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode('utf-8')).digest()).decode('utf-8').replace('=', '')
        return code_verifier, code_challenge

    @staticmethod
    def generate_auth_params():
        code_verifier, code_challenge = XService.generate_pkce_pair()
        state = secrets.token_urlsafe(32)
        return code_verifier, code_challenge, state

    @staticmethod
    def get_auth_url(code_challenge, state):
        # Note: PKCE uses client_id in URL, but token swap uses Basic Auth
        params = {
            "response_type": "code",
            "client_id": settings.X_CLIENT_ID,
            "redirect_uri": settings.X_REDIRECT_URI,
            "scope": " ".join(XConfig.SCOPES),
            "state": state,
            "code_challenge": code_challenge,
            "code_challenge_method": "S256"
        }
        return f"{XConfig.AUTH_URL}?{urlencode(params)}"

    @staticmethod
    def exchange_code_for_token(code, code_verifier):
        # FIX: Use Basic Auth Header, not body client_id
        headers = XService._get_auth_header()
        payload = {
            "code": code,
            "grant_type": "authorization_code",
            "redirect_uri": settings.X_REDIRECT_URI,
            "code_verifier": code_verifier,
        }
        
        res = requests.post(XConfig.TOKEN_URL, headers=headers, data=payload)
        data = res.json()
        if 'error' in data: 
            raise XAPIError(data.get('error_description'))
        
        expires_in = data.get('expires_in', 7200)
        return {
            "access_token": data['access_token'],
            "refresh_token": data['refresh_token'],
            "expires_at": timezone.now() + datetime.timedelta(seconds=expires_in)
        }

    @staticmethod
    def refresh_tokens(account):
        # FIX: Use Basic Auth Header
        headers = XService._get_auth_header()
        payload = {
            "grant_type": "refresh_token",
            "refresh_token": account.refresh_token,
        }
        
        res = requests.post(XConfig.TOKEN_URL, headers=headers, data=payload)
        data = res.json()
        
        if 'error' in data:
            account.is_active = False
            account.save()
            raise XAPIError(data.get('error_description'))
        
        account.access_token = data['access_token']
        if 'refresh_token' in data:
            account.refresh_token = data['refresh_token']
        
        account.expires_at = timezone.now() + datetime.timedelta(seconds=data.get('expires_in', 7200))
        account.save()

    @staticmethod
    def get_valid_token(account):
        if account.expires_at <= timezone.now() + datetime.timedelta(minutes=5):
            XService.refresh_tokens(account)
        return account.access_token

    @staticmethod
    def _make_request(endpoint, account, method="GET", payload=None):
        token = XService.get_valid_token(account)
        headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
        url = f"{XConfig.BASE_URL}/{endpoint}"
        
        try:
            if method == "GET":
                response = requests.get(url, headers=headers, params=payload)
            else:
                response = requests.post(url, headers=headers, json=payload)
            
            if response.status_code == 429:
                # FIX: Raise specific exception for Celery to handle, don't block worker
                reset_header = response.headers.get('x-rate-limit-reset')
                if reset_header:
                    reset_time = int(reset_header)
                    raise XRateLimitError(reset_time)
                else:
                    # Fallback if header missing
                    raise XRateLimitError(int(time.time()) + 60)
            
            if response.status_code >= 400:
                raise XAPIError(f"API Error {response.status_code}: {response.text}")
                
            return response.json()
        except requests.exceptions.RequestException as e:
            raise XAPIError(f"Network Error: {str(e)}")

    # --- DATA FETCHING HELPERS ---

    @staticmethod
    def _datetime_to_snowflake_id(dt):
        if not dt: return None
        timestamp_ms = int(dt.timestamp() * 1000)
        snowflake = (timestamp_ms - XService.TWITTER_EPOCH) << 22
        return str(snowflake)

    @staticmethod
    def _datetime_to_iso_string(dt):
        if not dt: return None
        if dt.tzinfo: dt = dt.astimezone(datetime.timezone.utc)
        return dt.strftime("%Y-%m-%dT%H:%M:%SZ")

    @staticmethod
    def _attach_expansions(data, response):
        """
        Maps users from 'includes' to their respective tweets in 'data'.
        This allows the tasks to simply access r_data['author'].
        """
        users = {u['id']: u for u in response.get('includes', {}).get('users', [])}
        media = {m['media_key']: m for m in response.get('includes', {}).get('media', [])}

        for item in data:
            item['author'] = users.get(item.get('author_id'))
            item['media_objects'] = [media[k] for k in item.get('attachments', {}).get('media_keys', []) if k in media]
        
        return data

    @staticmethod
    def get_user_tweets(account):
        tweets = []
        next_token = None
        while True:
            # FIX: Added expansions
            params = {
                "tweet.fields": "created_at,conversation_id,author_id,attachments,public_metrics", 
                "expansions": "author_id,attachments.media_keys",
                "user.fields": "username,name",
                "media.fields": "type,url,preview_image_url,alt_text",
                "max_results": 100,
                "exclude": "retweets"
            }
            if next_token: params['pagination_token'] = next_token
            
            data = XService._make_request(f"users/{account.platform_id}/tweets", account, "GET", params=params)
            raw_tweets = data.get('data', [])
            
            # Attach author/media data
            enriched_tweets = XService._attach_expansions(raw_tweets, data)
            tweets.extend(enriched_tweets)
            
            next_token = data.get('meta', {}).get('next_token')
            if not next_token: break
            time.sleep(0.5) # Small politeness delay between pages
        return tweets

    @staticmethod
    def fetch_tweet_replies(account, conversation_id, since_datetime=None, owner_id=None):
        use_enterprise = getattr(settings, 'X_USE_ENTERPRISE', False)
        endpoint = XConfig.SEARCH_RECENT_URL if not use_enterprise else XConfig.SEARCH_ALL_URL
        
        # Note: Free Tier (Basic) does not support Search. 
        # If this returns 403 on Basic Tier, you are on Free Tier which cannot search replies.
        next_token = None
        replies = []
            
        while True:
            query = f"conversation_id:{conversation_id}"
            if owner_id: query += f" to:{owner_id}"
            query += " -is:retweet"

            # FIX: Added expansions
            params = {
                "query": query,
                "tweet.fields": "created_at,author_id,text,referenced_tweets,in_reply_to_user_id",
                "expansions": "author_id",
                "user.fields": "username,name",
                "max_results": 100
            }
            
            if since_datetime:
                if use_enterprise:
                    params['start_time'] = XService._datetime_to_iso_string(since_datetime)
                else:
                    params['since_id'] = XService._datetime_to_snowflake_id(since_datetime)
            
            if next_token: params['pagination_token'] = next_token
            
            try:
                data = XService._make_request(endpoint, account, "GET", params=params)
                raw_replies = data.get('data', [])
                
                if not raw_replies: break
                
                # Attach authors so tasks can read .get('author')
                enriched_replies = XService._attach_expansions(raw_replies, data)
                replies.extend(enriched_replies)
                
                next_token = data.get('meta', {}).get('next_token')
                if not next_token: break
                
                time.sleep(0.5) # Politeness delay
                
            except XRateLimitError as e:
                # Re-raise this specific exception for the task to handle
                raise e
            except XAPIError as e:
                if "403" in str(e):
                    # Free tier limitation: Cannot use search endpoint
                    logger.warning(f"Search API Forbidden. Account might be on Free Tier.")
                break # Stop task on other errors to prevent loops
        
        return replies

    @staticmethod
    def post_reply(account, tweet_id, text):
        payload = {"text": text, "reply": {"in_reply_to_tweet_id": tweet_id}}
        return XService._make_request("tweets", account, "POST", payload=payload)