"""
Calendar Sync Service for RateRight
Handles synchronization with Google Calendar and Outlook
"""

import os
import json
import time
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import logging

# Google Calendar imports
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# Microsoft Graph API imports
import msal
import requests

from app.models import db
from app.models.booking import Booking
from app.models.availability import Availability
from app.models.user import User
from app.utils.encryption import token_encryptor

logger = logging.getLogger(__name__)

class CalendarSyncService:
    """Service for syncing with external calendars"""
    
    # Google Calendar Configuration
    GOOGLE_SCOPES = ['https://www.googleapis.com/auth/calendar']
    GOOGLE_CLIENT_CONFIG = {
        "web": {
            "client_id": os.environ.get('GOOGLE_CLIENT_ID'),
            "client_secret": os.environ.get('GOOGLE_CLIENT_SECRET'),
            "redirect_uris": [os.environ.get('GOOGLE_REDIRECT_URI', 'http://localhost:5000/api/calendar/google/callback')],
            "auth_uri": "https://accounts.google.com/o/oauth2/auth",
            "token_uri": "https://oauth2.googleapis.com/token"
        }
    }
    
    # Microsoft Configuration
    MICROSOFT_CONFIG = {
        'client_id': os.environ.get('MICROSOFT_CLIENT_ID'),
        'client_secret': os.environ.get('MICROSOFT_CLIENT_SECRET'),
        'authority': 'https://login.microsoftonline.com/common',
        'redirect_uri': os.environ.get('MICROSOFT_REDIRECT_URI', 'http://localhost:5000/api/calendar/outlook/callback'),
        'scope': ['https://graph.microsoft.com/Calendars.ReadWrite']
    }
    
    @classmethod
    def get_google_auth_url(cls, user_id: int) -> str:
        """Generate Google OAuth URL for calendar access"""
        try:
            flow = Flow.from_client_config(
                cls.GOOGLE_CLIENT_CONFIG,
                scopes=cls.GOOGLE_SCOPES
            )
            flow.redirect_uri = cls.GOOGLE_CLIENT_CONFIG['web']['redirect_uris'][0]
            
            auth_url, state = flow.authorization_url(
                access_type='offline',
                include_granted_scopes='true',
                state=str(user_id)
            )
            
            # Store state in session or cache for verification
            return auth_url
        except Exception as e:
            logger.error(f"Error generating Google auth URL: {str(e)}")
            raise
    
    @classmethod
    def handle_google_callback(cls, code: str, state: str) -> Dict:
        """Handle Google OAuth callback and store credentials"""
        try:
            flow = Flow.from_client_config(
                cls.GOOGLE_CLIENT_CONFIG,
                scopes=cls.GOOGLE_SCOPES
            )
            flow.redirect_uri = cls.GOOGLE_CLIENT_CONFIG['web']['redirect_uris'][0]
            
            # Exchange code for tokens
            flow.fetch_token(code=code)
            credentials = flow.credentials
            
            # Store credentials for user (state contains user_id)
            user_id = int(state)
            cls._store_google_credentials(user_id, credentials)
            
            return {
                'success': True,
                'message': 'Google Calendar connected successfully'
            }
        except Exception as e:
            logger.error(f"Error handling Google callback: {str(e)}")
            return {
                'success': False,
                'message': f'Failed to connect Google Calendar: {str(e)}'
            }
    
    @classmethod
    def get_microsoft_auth_url(cls, user_id: int) -> str:
        """Generate Microsoft OAuth URL for calendar access"""
        try:
            app = msal.ConfidentialClientApplication(
                cls.MICROSOFT_CONFIG['client_id'],
                authority=cls.MICROSOFT_CONFIG['authority'],
                client_credential=cls.MICROSOFT_CONFIG['client_secret']
            )
            
            auth_url = app.get_authorization_request_url(
                cls.MICROSOFT_CONFIG['scope'],
                state=str(user_id),
                redirect_uri=cls.MICROSOFT_CONFIG['redirect_uri']
            )
            
            return auth_url
        except Exception as e:
            logger.error(f"Error generating Microsoft auth URL: {str(e)}")
            raise
    
    @classmethod
    def handle_microsoft_callback(cls, code: str, state: str) -> Dict:
        """Handle Microsoft OAuth callback and store credentials"""
        try:
            app = msal.ConfidentialClientApplication(
                cls.MICROSOFT_CONFIG['client_id'],
                authority=cls.MICROSOFT_CONFIG['authority'],
                client_credential=cls.MICROSOFT_CONFIG['client_secret']
            )
            
            result = app.acquire_token_by_authorization_code(
                code,
                scopes=cls.MICROSOFT_CONFIG['scope'],
                redirect_uri=cls.MICROSOFT_CONFIG['redirect_uri']
            )
            
            if 'access_token' in result:
                user_id = int(state)
                cls._store_microsoft_credentials(user_id, result)
                
                return {
                    'success': True,
                    'message': 'Outlook Calendar connected successfully'
                }
            else:
                raise Exception(result.get('error_description', 'Unknown error'))
                
        except Exception as e:
            logger.error(f"Error handling Microsoft callback: {str(e)}")
            return {
                'success': False,
                'message': f'Failed to connect Outlook Calendar: {str(e)}'
            }
    
    @classmethod
    def upsert_booking_event(cls, user_id: int, booking) -> str:
        """Create or update a Google Calendar event for a booking"""
        try:
            # Check if user has calendar sync enabled
            user = User.query.get(user_id)
            if not user or not user.calendar_sync_enabled:
                logger.info(f"Calendar sync disabled for user {user_id}")
                return None
            
            # Get user's Google credentials
            credentials = cls._get_google_credentials(user_id)
            if not credentials:
                logger.warning(f"No Google credentials for user {user_id}")
                return None
            
            # Refresh credentials if needed
            from google.auth.transport.requests import Request
            if credentials.expired and credentials.refresh_token:
                credentials.refresh(Request())
                cls._store_google_credentials(user_id, credentials)
            
            # Build Google Calendar service
            service = build('calendar', 'v3', credentials=credentials)
            
            # Build event body
            summary = f"{booking.service_type or 'RateRight Job'}"
            if hasattr(booking, 'worker') and hasattr(booking, 'client'):
                summary += f" • {booking.worker.first_name} ↔ {booking.client.first_name}"
            
            # Combine booking date and time for datetime
            from datetime import datetime, timezone
            start_datetime = datetime.combine(booking.booking_date, booking.start_time)
            end_datetime = datetime.combine(booking.booking_date, booking.end_time)
            
            event = {
                'summary': summary,
                'description': f"Booking #{booking.id}\nService: {booking.service_type}\nLocation: {booking.location or 'TBD'}\nNotes: {booking.description or booking.client_notes or ''}",
                'start': {
                    'dateTime': start_datetime.isoformat(),
                    'timeZone': 'Australia/Sydney',
                },
                'end': {
                    'dateTime': end_datetime.isoformat(),
                    'timeZone': 'Australia/Sydney',
                },
                'location': booking.location or '',
                'reminders': {
                    'useDefault': False,
                    'overrides': [
                        {'method': 'email', 'minutes': 24 * 60},  # 1 day before
                        {'method': 'popup', 'minutes': 120},  # 2 hours before
                        {'method': 'popup', 'minutes': 30},  # 30 minutes before
                    ],
                },
            }
            
            # Add attendees if emails available
            attendees = []
            if hasattr(booking, 'client') and hasattr(booking.client, 'email'):
                attendees.append({'email': booking.client.email})
            if hasattr(booking, 'worker') and hasattr(booking.worker, 'email'):
                attendees.append({'email': booking.worker.email})
            if attendees:
                event['attendees'] = attendees
            
            # Insert or update event with retry logic
            if booking.google_event_id:
                # Update existing event with retries
                result = cls._execute_with_retry(
                    lambda: service.events().patch(
                        calendarId='primary',
                        eventId=booking.google_event_id,
                        body=event
                    ).execute()
                )
                logger.info(f"Updated Google Calendar event {booking.google_event_id} for booking {booking.id}")
            else:
                # Create new event with retries
                result = cls._execute_with_retry(
                    lambda: service.events().insert(
                        calendarId='primary',
                        body=event
                    ).execute()
                )
                
                # Store event ID
                booking.google_event_id = result['id']
                db.session.commit()
                logger.info(f"Created Google Calendar event {result['id']} for booking {booking.id}")
            
            return result['id']
            
        except HttpError as e:
            logger.error(f"Google Calendar API error: {str(e)}")
            return None
        except Exception as e:
            logger.error(f"Error syncing booking to Google Calendar: {str(e)}")
            return None
    
    @classmethod
    def delete_booking_event(cls, user_id: int, booking) -> bool:
        """Delete a Google Calendar event for a booking"""
        try:
            if not booking.google_event_id:
                logger.info(f"No Google event to delete for booking {booking.id}")
                return True
            
            # Get user's Google credentials
            credentials = cls._get_google_credentials(user_id)
            if not credentials:
                logger.warning(f"No Google credentials for user {user_id}")
                return False
            
            # Refresh credentials if needed
            from google.auth.transport.requests import Request
            if credentials.expired and credentials.refresh_token:
                credentials.refresh(Request())
                cls._store_google_credentials(user_id, credentials)
            
            # Build Google Calendar service
            service = build('calendar', 'v3', credentials=credentials)
            
            # Delete the event
            service.events().delete(
                calendarId='primary',
                eventId=booking.google_event_id
            ).execute()
            
            # Clear the event ID
            booking.google_event_id = None
            db.session.commit()
            
            logger.info(f"Deleted Google Calendar event for booking {booking.id}")
            return True
            
        except HttpError as e:
            if e.resp.status == 404:
                # Event doesn't exist, clear the ID anyway
                booking.google_event_id = None
                db.session.commit()
                logger.warning(f"Google Calendar event not found for booking {booking.id}")
                return True
            logger.error(f"Google Calendar API error: {str(e)}")
            return False
        except Exception as e:
            logger.error(f"Error deleting Google Calendar event: {str(e)}")
            return False
    
    @classmethod
    def sync_booking_to_google(cls, booking_id: int) -> bool:
        """Legacy method - redirects to upsert_booking_event"""
        try:
            booking = Booking.query.get(booking_id)
            if not booking:
                return False
            
            result = cls.upsert_booking_event(booking.worker_id, booking)
            return result is not None
            
        except Exception as e:
            logger.error(f"Error in sync_booking_to_google: {str(e)}")
            return False
    
    @classmethod
    def sync_booking_to_outlook(cls, booking_id: int) -> bool:
        """Sync a booking to Outlook Calendar"""
        try:
            booking = Booking.query.get(booking_id)
            if not booking:
                return False
            
            # Get user's Microsoft credentials
            credentials = cls._get_microsoft_credentials(booking.worker_id)
            if not credentials:
                logger.warning(f"No Microsoft credentials for user {booking.worker_id}")
                return False
            
            # Prepare event data
            event_data = {
                'subject': f'RateRight Booking: {booking.client.username}',
                'body': {
                    'contentType': 'HTML',
                    'content': booking.service_description or 'RateRight booking'
                },
                'start': {
                    'dateTime': booking.scheduled_datetime.isoformat(),
                    'timeZone': 'Australia/Sydney'
                },
                'end': {
                    'dateTime': (booking.scheduled_datetime + timedelta(hours=booking.duration_hours)).isoformat(),
                    'timeZone': 'Australia/Sydney'
                },
                'reminderMinutesBeforeStart': 60,
                'isReminderOn': True
            }
            
            headers = {
                'Authorization': f"Bearer {credentials['access_token']}",
                'Content-Type': 'application/json'
            }
            
            if booking.outlook_event_id:
                # Update existing event
                response = requests.patch(
                    f"https://graph.microsoft.com/v1.0/me/events/{booking.outlook_event_id}",
                    headers=headers,
                    json=event_data
                )
            else:
                # Create new event
                response = requests.post(
                    'https://graph.microsoft.com/v1.0/me/events',
                    headers=headers,
                    json=event_data
                )
                
                if response.status_code == 201:
                    event = response.json()
                    booking.outlook_event_id = event['id']
                    db.session.commit()
            
            if response.status_code in [200, 201]:
                logger.info(f"Synced booking {booking_id} to Outlook Calendar")
                return True
            else:
                logger.error(f"Outlook API error: {response.text}")
                return False
                
        except Exception as e:
            logger.error(f"Error syncing to Outlook Calendar: {str(e)}")
            return False
    
    @classmethod
    def sync_availability_to_google(cls, availability_id: int) -> bool:
        """Sync availability slots to Google Calendar"""
        try:
            availability = Availability.query.get(availability_id)
            if not availability or availability.status != 'available':
                return False
            
            credentials = cls._get_google_credentials(availability.worker_id)
            if not credentials:
                return False
            
            service = build('calendar', 'v3', credentials=credentials)
            
            # Create availability event
            event = {
                'summary': 'Available on RateRight',
                'description': 'Available for bookings on RateRight platform',
                'start': {
                    'dateTime': availability.start_datetime.isoformat(),
                    'timeZone': 'Australia/Sydney',
                },
                'end': {
                    'dateTime': availability.end_datetime.isoformat(),
                    'timeZone': 'Australia/Sydney',
                },
                'transparency': 'transparent',  # Show as available
                'colorId': '2'  # Green color
            }
            
            if availability.google_event_id:
                service.events().update(
                    calendarId='primary',
                    eventId=availability.google_event_id,
                    body=event
                ).execute()
            else:
                event = service.events().insert(
                    calendarId='primary',
                    body=event
                ).execute()
                availability.google_event_id = event['id']
                db.session.commit()
            
            return True
            
        except Exception as e:
            logger.error(f"Error syncing availability to Google: {str(e)}")
            return False
    
    @classmethod
    def remove_from_google(cls, event_id: str, user_id: int) -> bool:
        """Remove an event from Google Calendar"""
        try:
            credentials = cls._get_google_credentials(user_id)
            if not credentials:
                return False
            
            service = build('calendar', 'v3', credentials=credentials)
            service.events().delete(
                calendarId='primary',
                eventId=event_id
            ).execute()
            
            return True
        except Exception as e:
            logger.error(f"Error removing from Google Calendar: {str(e)}")
            return False
    
    @classmethod
    def remove_from_outlook(cls, event_id: str, user_id: int) -> bool:
        """Remove an event from Outlook Calendar"""
        try:
            credentials = cls._get_microsoft_credentials(user_id)
            if not credentials:
                return False
            
            headers = {
                'Authorization': f"Bearer {credentials['access_token']}"
            }
            
            response = requests.delete(
                f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
                headers=headers
            )
            
            return response.status_code == 204
        except Exception as e:
            logger.error(f"Error removing from Outlook Calendar: {str(e)}")
            return False
    
    @classmethod
    def import_from_google(cls, user_id: int, start_date: datetime, end_date: datetime) -> List[Dict]:
        """Import events from Google Calendar"""
        try:
            credentials = cls._get_google_credentials(user_id)
            if not credentials:
                return []
            
            service = build('calendar', 'v3', credentials=credentials)
            
            events_result = service.events().list(
                calendarId='primary',
                timeMin=start_date.isoformat() + 'Z',
                timeMax=end_date.isoformat() + 'Z',
                singleEvents=True,
                orderBy='startTime'
            ).execute()
            
            events = events_result.get('items', [])
            
            # Convert to our format
            imported_events = []
            for event in events:
                start = event['start'].get('dateTime', event['start'].get('date'))
                end = event['end'].get('dateTime', event['end'].get('date'))
                
                imported_events.append({
                    'title': event.get('summary', 'Untitled'),
                    'description': event.get('description', ''),
                    'start': start,
                    'end': end,
                    'source': 'google',
                    'external_id': event['id']
                })
            
            return imported_events
            
        except Exception as e:
            logger.error(f"Error importing from Google Calendar: {str(e)}")
            return []
    
    @classmethod
    def import_from_outlook(cls, user_id: int, start_date: datetime, end_date: datetime) -> List[Dict]:
        """Import events from Outlook Calendar"""
        try:
            credentials = cls._get_microsoft_credentials(user_id)
            if not credentials:
                return []
            
            headers = {
                'Authorization': f"Bearer {credentials['access_token']}"
            }
            
            params = {
                '$filter': f"start/dateTime ge '{start_date.isoformat()}' and end/dateTime le '{end_date.isoformat()}'",
                '$orderby': 'start/dateTime',
                '$select': 'subject,body,start,end,id'
            }
            
            response = requests.get(
                'https://graph.microsoft.com/v1.0/me/events',
                headers=headers,
                params=params
            )
            
            if response.status_code != 200:
                return []
            
            events = response.json().get('value', [])
            
            # Convert to our format
            imported_events = []
            for event in events:
                imported_events.append({
                    'title': event.get('subject', 'Untitled'),
                    'description': event.get('body', {}).get('content', ''),
                    'start': event['start']['dateTime'],
                    'end': event['end']['dateTime'],
                    'source': 'outlook',
                    'external_id': event['id']
                })
            
            return imported_events
            
        except Exception as e:
            logger.error(f"Error importing from Outlook Calendar: {str(e)}")
            return []
    
    @classmethod
    def _store_google_credentials(cls, user_id: int, credentials: Credentials):
        """Store encrypted Google credentials for a user"""
        user = User.query.get(user_id)
        if user:
            token_data = {
                'google': {
                    'token': credentials.token,
                    'refresh_token': credentials.refresh_token,
                    'token_uri': credentials.token_uri,
                    'client_id': credentials.client_id,
                    'client_secret': credentials.client_secret,
                    'scopes': credentials.scopes,
                    'last_refreshed': datetime.utcnow().isoformat()
                }
            }
            
            # Encrypt the token data
            user.calendar_sync_data_encrypted = token_encryptor.encrypt_token(token_data)
            user.calendar_sync_enabled = True
            user.calendar_last_sync = datetime.utcnow()
            db.session.commit()
    
    @classmethod
    def _get_google_credentials(cls, user_id: int) -> Optional[Credentials]:
        """Retrieve and decrypt Google credentials for a user"""
        user = User.query.get(user_id)
        if not user or not user.calendar_sync_data_encrypted:
            return None
        
        # Decrypt the token data
        token_data = token_encryptor.decrypt_token(user.calendar_sync_data_encrypted)
        if not token_data or 'google' not in token_data:
            return None
        
        google_creds = token_data['google']
        
        return Credentials(
            token=google_creds['token'],
            refresh_token=google_creds['refresh_token'],
            token_uri=google_creds['token_uri'],
            client_id=google_creds['client_id'],
            client_secret=google_creds['client_secret'],
            scopes=google_creds['scopes']
        )
    
    @classmethod
    def _store_microsoft_credentials(cls, user_id: int, token_response: Dict):
        """Store encrypted Microsoft credentials for a user"""
        user = User.query.get(user_id)
        if user:
            # Get existing encrypted data if any
            existing_data = {}
            if user.calendar_sync_data_encrypted:
                existing_data = token_encryptor.decrypt_token(user.calendar_sync_data_encrypted) or {}
            
            existing_data['microsoft'] = {
                'access_token': token_response['access_token'],
                'refresh_token': token_response.get('refresh_token'),
                'expires_in': token_response.get('expires_in'),
                'token_type': token_response.get('token_type'),
                'last_refreshed': datetime.utcnow().isoformat()
            }
            
            # Encrypt and store
            user.calendar_sync_data_encrypted = token_encryptor.encrypt_token(existing_data)
            user.calendar_sync_enabled = True
            user.calendar_last_sync = datetime.utcnow()
            db.session.commit()
    
    @classmethod
    def _get_microsoft_credentials(cls, user_id: int) -> Optional[Dict]:
        """Retrieve and decrypt Microsoft credentials for a user"""
        user = User.query.get(user_id)
        if not user or not user.calendar_sync_data_encrypted:
            return None
        
        # Decrypt the token data
        token_data = token_encryptor.decrypt_token(user.calendar_sync_data_encrypted)
        if not token_data:
            return None
        
        return token_data.get('microsoft')
    
    @classmethod
    def _execute_with_retry(cls, func, max_retries=3):
        """Execute a function with exponential backoff retry on 429/5xx errors"""
        for attempt in range(max_retries):
            try:
                return func()
            except HttpError as e:
                if e.resp.status in [429, 500, 502, 503, 504]:
                    if attempt < max_retries - 1:
                        # Exponential backoff: 1s, 2s, 4s
                        wait_time = 2 ** attempt
                        logger.warning(f"Google API error {e.resp.status}, retrying in {wait_time}s...")
                        time.sleep(wait_time)
                        continue
                raise
            except Exception:
                raise
        return None
    
    @classmethod
    def disconnect_google_calendar(cls, user_id: int) -> bool:
        """Disconnect Google Calendar and revoke access"""
        try:
            user = User.query.get(user_id)
            if not user:
                return False
            
            # Get credentials to revoke
            credentials = cls._get_google_credentials(user_id)
            if credentials:
                try:
                    # Revoke the token
                    import requests as req
                    req.post('https://oauth2.googleapis.com/revoke',
                            params={'token': credentials.token},
                            headers={'content-type': 'application/x-www-form-urlencoded'})
                except Exception as e:
                    logger.warning(f"Failed to revoke Google token: {e}")
            
            # Clear stored credentials
            user.calendar_sync_enabled = False
            user.calendar_sync_data_encrypted = None
            user.calendar_last_sync = None
            
            # Clear Google event IDs from all bookings
            from app.models.booking import Booking
            Booking.query.filter_by(worker_id=user_id).update({'google_event_id': None})
            
            db.session.commit()
            logger.info(f"Disconnected Google Calendar for user {user_id}")
            return True
            
        except Exception as e:
            logger.error(f"Error disconnecting Google Calendar: {str(e)}")
            return False
    
    @classmethod
    def disconnect_outlook_calendar(cls, user_id: int) -> bool:
        """Disconnect Outlook Calendar"""
        try:
            user = User.query.get(user_id)
            if not user:
                return False
            
            # Clear stored credentials
            if user.calendar_sync_data_encrypted:
                token_data = token_encryptor.decrypt_token(user.calendar_sync_data_encrypted) or {}
                if 'microsoft' in token_data:
                    del token_data['microsoft']
                    if token_data:  # Still has Google credentials
                        user.calendar_sync_data_encrypted = token_encryptor.encrypt_token(token_data)
                    else:  # No credentials left
                        user.calendar_sync_enabled = False
                        user.calendar_sync_data_encrypted = None
            
            # Clear Outlook event IDs from all bookings
            from app.models.booking import Booking
            Booking.query.filter_by(worker_id=user_id).update({'outlook_event_id': None})
            
            db.session.commit()
            logger.info(f"Disconnected Outlook Calendar for user {user_id}")
            return True
            
        except Exception as e:
            logger.error(f"Error disconnecting Outlook Calendar: {str(e)}")
            return False
