# -*- coding: utf-8 -*-

import json
import logging
import time
import requests
from datetime import datetime, timedelta

from odoo import api, fields, models, _
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

TIMEOUT = 20
MAX_THROTTLE_RETRIES = 3
DEFAULT_RETRY_SECONDS = 10

# Google OAuth endpoints
GOOGLE_AUTH_ENDPOINT = 'https://accounts.google.com/o/oauth2/auth'
GOOGLE_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token'
GOOGLE_CALENDAR_API = 'https://www.googleapis.com/calendar/v3'
GOOGLE_USERINFO_API = 'https://www.googleapis.com/oauth2/v2/userinfo'
GOOGLE_SCOPES = 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/userinfo.email'

# Microsoft OAuth endpoints
MICROSOFT_AUTH_ENDPOINT = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize'
MICROSOFT_TOKEN_ENDPOINT = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
MICROSOFT_GRAPH_API = 'https://graph.microsoft.com/v1.0'
MICROSOFT_SCOPES = 'offline_access openid Calendars.ReadWrite User.Read'
MICROSOFT_SELECT_FIELDS = ','.join([
    'id', 'subject', 'bodyPreview', 'body', 'start', 'end',
    'location', 'isAllDay', 'sensitivity', 'showAs',
    'iCalUId', 'isCancelled',
])


class FusionCalendarAccount(models.Model):
    _name = 'fusion.calendar.account'
    _description = 'Connected Calendar Account'
    _order = 'x_fc_provider, x_fc_email'

    x_fc_user_id = fields.Many2one(
        'res.users', string='User', required=True,
        ondelete='cascade', default=lambda self: self.env.user, index=True,
    )
    x_fc_provider = fields.Selection([
        ('google', 'Google'),
        ('microsoft', 'Microsoft'),
    ], string='Provider', required=True)
    x_fc_email = fields.Char(string='Account Email')
    x_fc_name = fields.Char(string='Account Label', compute='_compute_name', store=True)
    x_fc_active = fields.Boolean(string='Active', default=True)

    # OAuth tokens — restricted to system group
    x_fc_rtoken = fields.Char(string='Refresh Token', groups='base.group_system')
    x_fc_token = fields.Char(string='Access Token', groups='base.group_system')
    x_fc_token_validity = fields.Datetime(string='Token Expiry', groups='base.group_system')

    # Sync state
    x_fc_sync_token = fields.Char(string='Sync Token', groups='base.group_system')
    x_fc_calendar_id = fields.Char(string='Calendar ID', default='primary')
    x_fc_last_sync = fields.Datetime(string='Last Sync')
    x_fc_sync_status = fields.Selection([
        ('active', 'Active'),
        ('error', 'Error'),
        ('paused', 'Paused'),
    ], string='Sync Status', default='active')
    x_fc_error_message = fields.Text(string='Last Error')

    # Links
    x_fc_link_ids = fields.One2many(
        'fusion.calendar.event.link', 'x_fc_account_id', string='Event Links',
    )

    @api.depends('x_fc_provider', 'x_fc_email')
    def _compute_name(self):
        for rec in self:
            provider_label = dict(self._fields['x_fc_provider'].selection).get(rec.x_fc_provider, '')
            rec.x_fc_name = '%s — %s' % (provider_label, rec.x_fc_email or 'Not connected')

    # -------------------------------------------------------------------------
    # OAuth Credentials — fallback to Odoo defaults
    # -------------------------------------------------------------------------

    def _get_google_client_id(self):
        ICP = self.env['ir.config_parameter'].sudo()
        return (
            ICP.get_param('fusion_schedule_google_client_id')
            or ICP.get_param('google_calendar_client_id', '')
        )

    def _get_google_client_secret(self):
        ICP = self.env['ir.config_parameter'].sudo()
        return (
            ICP.get_param('fusion_schedule_google_client_secret')
            or ICP.get_param('google_calendar_client_secret', '')
        )

    def _get_microsoft_client_id(self):
        ICP = self.env['ir.config_parameter'].sudo()
        return (
            ICP.get_param('fusion_schedule_microsoft_client_id')
            or ICP.get_param('microsoft_calendar_client_id', '')
        )

    def _get_microsoft_client_secret(self):
        ICP = self.env['ir.config_parameter'].sudo()
        return (
            ICP.get_param('fusion_schedule_microsoft_client_secret')
            or ICP.get_param('microsoft_calendar_client_secret', '')
        )

    # -------------------------------------------------------------------------
    # OAuth Token Management
    # -------------------------------------------------------------------------

    def _get_valid_token(self):
        """Return a valid access token, refreshing if necessary."""
        self.ensure_one()
        if not self.sudo().x_fc_rtoken:
            raise UserError(_("No refresh token. Please reconnect this calendar account."))

        # Check if current token is still valid (with 1 min buffer)
        if (self.sudo().x_fc_token_validity
                and self.sudo().x_fc_token_validity >= fields.Datetime.now() + timedelta(minutes=1)):
            return self.sudo().x_fc_token

        return self._refresh_token()

    def _refresh_token(self):
        """Refresh the access token using the refresh token."""
        self.ensure_one()
        try:
            if self.x_fc_provider == 'google':
                return self._refresh_google_token()
            elif self.x_fc_provider == 'microsoft':
                return self._refresh_microsoft_token()
        except requests.HTTPError as e:
            if e.response and e.response.status_code in (400, 401):
                self.sudo().write({
                    'x_fc_token': False,
                    'x_fc_token_validity': False,
                    'x_fc_sync_status': 'error',
                    'x_fc_error_message': _('Token expired or revoked. Please reconnect this account.'),
                })
                return False
            raise

    def _refresh_google_token(self):
        """Refresh Google OAuth token."""
        self.ensure_one()
        data = {
            'client_id': self._get_google_client_id(),
            'client_secret': self._get_google_client_secret(),
            'refresh_token': self.sudo().x_fc_rtoken,
            'grant_type': 'refresh_token',
        }
        resp = requests.post(GOOGLE_TOKEN_ENDPOINT, data=data, timeout=TIMEOUT)
        resp.raise_for_status()
        token_data = resp.json()

        access_token = token_data['access_token']
        expires_in = token_data.get('expires_in', 3600)
        self.sudo().write({
            'x_fc_token': access_token,
            'x_fc_token_validity': fields.Datetime.now() + timedelta(seconds=expires_in),
        })
        return access_token

    def _refresh_microsoft_token(self):
        """Refresh Microsoft OAuth token."""
        self.ensure_one()
        data = {
            'client_id': self._get_microsoft_client_id(),
            'client_secret': self._get_microsoft_client_secret(),
            'refresh_token': self.sudo().x_fc_rtoken,
            'grant_type': 'refresh_token',
        }
        resp = requests.post(MICROSOFT_TOKEN_ENDPOINT, data=data, timeout=TIMEOUT)
        resp.raise_for_status()
        token_data = resp.json()

        access_token = token_data['access_token']
        expires_in = token_data.get('expires_in', 3600)
        # Microsoft may return a new refresh token
        new_rtoken = token_data.get('refresh_token')
        vals = {
            'x_fc_token': access_token,
            'x_fc_token_validity': fields.Datetime.now() + timedelta(seconds=expires_in),
        }
        if new_rtoken:
            vals['x_fc_rtoken'] = new_rtoken
        self.sudo().write(vals)
        return access_token

    # -------------------------------------------------------------------------
    # OAuth Code Exchange (called from controller callback)
    # -------------------------------------------------------------------------

    def _exchange_google_code(self, code, redirect_uri):
        """Exchange Google auth code for tokens."""
        data = {
            'code': code,
            'client_id': self._get_google_client_id(),
            'client_secret': self._get_google_client_secret(),
            'redirect_uri': redirect_uri,
            'grant_type': 'authorization_code',
        }
        resp = requests.post(GOOGLE_TOKEN_ENDPOINT, data=data, timeout=TIMEOUT)
        resp.raise_for_status()
        return resp.json()

    def _exchange_microsoft_code(self, code, redirect_uri):
        """Exchange Microsoft auth code for tokens."""
        data = {
            'code': code,
            'client_id': self._get_microsoft_client_id(),
            'client_secret': self._get_microsoft_client_secret(),
            'redirect_uri': redirect_uri,
            'grant_type': 'authorization_code',
            'scope': MICROSOFT_SCOPES,
        }
        resp = requests.post(MICROSOFT_TOKEN_ENDPOINT, data=data, timeout=TIMEOUT)
        resp.raise_for_status()
        return resp.json()

    # -------------------------------------------------------------------------
    # Fetch Account Info
    # -------------------------------------------------------------------------

    @api.model
    def _fetch_google_email(self, access_token):
        """Get the email of the authenticated Google account."""
        resp = requests.get(
            GOOGLE_USERINFO_API,
            headers={'Authorization': 'Bearer %s' % access_token},
            timeout=TIMEOUT,
        )
        resp.raise_for_status()
        return resp.json().get('email', '')

    @api.model
    def _fetch_microsoft_email(self, access_token):
        """Get the email of the authenticated Microsoft account."""
        resp = requests.get(
            '%s/me' % MICROSOFT_GRAPH_API,
            headers={'Authorization': 'Bearer %s' % access_token},
            timeout=TIMEOUT,
        )
        resp.raise_for_status()
        data = resp.json()
        return data.get('mail') or data.get('userPrincipalName', '')

    # -------------------------------------------------------------------------
    # Sync Engine — Pull (External → Odoo)
    # -------------------------------------------------------------------------

    def _sync_pull(self):
        """Pull events from the external calendar into Odoo."""
        self.ensure_one()
        _logger.warning("Sync pull starting for account %s (%s)", self.id, self.x_fc_email)
        token = self._get_valid_token()
        if not token:
            _logger.error("No valid token for account %s (%s), skipping sync", self.id, self.x_fc_email)
            return False

        try:
            if self.x_fc_provider == 'google':
                return self._sync_pull_google(token)
            elif self.x_fc_provider == 'microsoft':
                return self._sync_pull_microsoft(token)
        except requests.HTTPError as e:
            error_msg = str(e)
            if e.response is not None:
                try:
                    error_msg = e.response.json().get('error', {}).get('message', str(e))
                except Exception:
                    error_msg = e.response.text[:500]
            _logger.exception("Sync pull error for account %s: %s", self.id, error_msg)
            self.sudo().write({
                'x_fc_sync_status': 'error',
                'x_fc_error_message': error_msg[:500],
            })
            return False
        except Exception as e:
            _logger.exception("Sync pull error for account %s: %s", self.id, e)
            self.sudo().write({
                'x_fc_sync_status': 'error',
                'x_fc_error_message': str(e)[:500],
            })
            return False

    def _sync_pull_google(self, token):
        """Pull events from Google Calendar."""
        self.ensure_one()
        calendar_id = self.x_fc_calendar_id or 'primary'
        url = '%s/calendars/%s/events' % (GOOGLE_CALENDAR_API, calendar_id)
        params = {
            'singleEvents': 'true',
            'maxResults': 100,
        }

        if self.x_fc_sync_token:
            params['syncToken'] = self.x_fc_sync_token
        else:
            time_min = (datetime.utcnow() - timedelta(days=14)).isoformat() + 'Z'
            time_max = (datetime.utcnow() + timedelta(days=30)).isoformat() + 'Z'
            params['timeMin'] = time_min
            params['timeMax'] = time_max

        headers = {'Authorization': 'Bearer %s' % token}
        all_events = []
        next_sync_token = self.x_fc_sync_token

        while True:
            resp = self._google_request_with_retry(url, params, headers)

            if resp.status_code == 410:
                _logger.info("Google sync token expired for account %s, doing full sync", self.id)
                self.sudo().x_fc_sync_token = False
                return self._sync_pull_google(token)

            resp.raise_for_status()
            data = resp.json()

            all_events.extend(data.get('items', []))
            next_page_token = data.get('nextPageToken')
            if next_page_token:
                params['pageToken'] = next_page_token
                params.pop('syncToken', None)
                params.pop('timeMin', None)
                params.pop('timeMax', None)
            else:
                next_sync_token = data.get('nextSyncToken', next_sync_token)
                break

        created = 0
        updated = 0
        deleted = 0
        for event_data in all_events:
            result = self._process_google_event(event_data)
            if result == 'created':
                created += 1
            elif result == 'updated':
                updated += 1
            elif result == 'deleted':
                deleted += 1

        self.sudo().write({
            'x_fc_sync_token': next_sync_token,
            'x_fc_last_sync': fields.Datetime.now(),
            'x_fc_sync_status': 'active',
            'x_fc_error_message': False,
        })

        _logger.info(
            "Google sync for account %s (%s): %d created, %d updated, %d deleted",
            self.id, self.x_fc_email, created, updated, deleted,
        )
        return True

    def _google_request_with_retry(self, url, params, headers):
        """GET request with automatic retry on 429 / 503 and connection errors."""
        for attempt in range(MAX_THROTTLE_RETRIES + 1):
            try:
                resp = requests.get(url, params=params, headers=headers, timeout=TIMEOUT)
            except (requests.ConnectionError, requests.Timeout) as e:
                wait = min(DEFAULT_RETRY_SECONDS * (attempt + 1), 30)
                _logger.warning(
                    "Google connection error for account %s: %s (retry in %ds, attempt %d/%d)",
                    self.id, e, wait, attempt + 1, MAX_THROTTLE_RETRIES,
                )
                if attempt < MAX_THROTTLE_RETRIES:
                    time.sleep(wait)
                    continue
                raise
            if resp.status_code not in (429, 503):
                return resp
            retry_after = int(resp.headers.get('Retry-After', DEFAULT_RETRY_SECONDS))
            retry_after = min(retry_after, 60)
            _logger.warning(
                "Google throttled account %s (HTTP %s), retry in %ds (attempt %d/%d)",
                self.id, resp.status_code, retry_after, attempt + 1, MAX_THROTTLE_RETRIES,
            )
            if attempt < MAX_THROTTLE_RETRIES:
                time.sleep(retry_after)
        return resp

    def _silent_ctx(self):
        """Context flags to suppress all email notifications during sync."""
        return {
            'no_calendar_sync': True,
            'dont_notify': True,
            'mail_create_nosubscribe': True,
            'mail_create_nolog': True,
            'no_mail_notification': True,
            'no_mail_to_attendees': True,
            'skip_attendee_notification': True,
        }

    def _find_existing_event(self, CalendarEvent, vals):
        """Find an existing calendar event matching name+start+stop to avoid duplicates."""
        start_val = vals.get('start') or vals.get('start_date')
        stop_val = vals.get('stop') or vals.get('stop_date')
        if not (start_val and stop_val and vals.get('name')):
            return None
        domain = [('name', '=', vals['name']), ('active', '=', True)]
        if vals.get('allday'):
            domain += [('start_date', '=', start_val), ('stop_date', '=', stop_val)]
        else:
            domain += [('start', '=', start_val), ('stop', '=', stop_val)]
        return CalendarEvent.search(domain, limit=1)

    def _upsert_event_link(self, EventLink, odoo_event_id, external_id, ical_uid):
        """Create or update a link between an Odoo event and an external event.

        If this account already has a link to the same Odoo event, update the
        external_id rather than creating a duplicate link row.  Returns the
        link record.
        """
        existing = EventLink.search([
            ('x_fc_account_id', '=', self.id),
            ('x_fc_event_id', '=', odoo_event_id),
        ], limit=1)
        now = fields.Datetime.now()
        if existing:
            existing.write({
                'x_fc_external_id': external_id,
                'x_fc_universal_id': ical_uid or existing.x_fc_universal_id,
                'x_fc_last_synced': now,
            })
            return existing
        return EventLink.create({
            'x_fc_event_id': odoo_event_id,
            'x_fc_account_id': self.id,
            'x_fc_external_id': external_id,
            'x_fc_universal_id': ical_uid,
            'x_fc_sync_direction': 'pull',
            'x_fc_last_synced': now,
        })

    def _process_google_event(self, event_data):
        """Process a single Google Calendar event."""
        self.ensure_one()
        external_id = event_data.get('id')
        if not external_id:
            return None

        ctx = self._silent_ctx()
        EventLink = self.env['fusion.calendar.event.link'].sudo()
        CalendarEvent = self.env['calendar.event'].sudo().with_context(**ctx)

        link = EventLink.search([
            ('x_fc_account_id', '=', self.id),
            ('x_fc_external_id', '=', external_id),
        ], limit=1)

        status = event_data.get('status', 'confirmed')
        if status == 'cancelled':
            if link and link.x_fc_event_id:
                link.x_fc_event_id.with_context(**ctx).write({'active': False})
                link.unlink()
            return 'deleted'

        vals = self._google_event_to_odoo_vals(event_data)
        if not vals:
            return None

        ical_uid = event_data.get('iCalUID', '')

        if link:
            if link.x_fc_event_id and link.x_fc_event_id.active:
                link.x_fc_event_id.with_context(**ctx).write(vals)
                link.write({'x_fc_last_synced': fields.Datetime.now()})
            return 'updated'

        existing_link = EventLink.search([
            ('x_fc_universal_id', '=', ical_uid),
            ('x_fc_universal_id', '!=', False),
        ], limit=1) if ical_uid else None

        if existing_link and existing_link.x_fc_event_id:
            self._upsert_event_link(EventLink, existing_link.x_fc_event_id.id, external_id, ical_uid)
            return 'updated'

        reuse_event = self._find_existing_event(CalendarEvent, vals)
        if reuse_event:
            self._upsert_event_link(EventLink, reuse_event.id, external_id, ical_uid)
            return 'updated'

        vals['partner_ids'] = [(4, self.x_fc_user_id.partner_id.id)]
        event = CalendarEvent.create(vals)
        self._upsert_event_link(EventLink, event.id, external_id, ical_uid)
        return 'created'

    def _google_event_to_odoo_vals(self, event_data):
        """Convert Google Calendar event dict to Odoo calendar.event vals."""
        start_data = event_data.get('start', {})
        end_data = event_data.get('end', {})

        if not start_data or not end_data:
            return None

        allday = 'date' in start_data
        if allday:
            try:
                start_dt = datetime.strptime(start_data['date'], '%Y-%m-%d')
                end_dt = datetime.strptime(end_data['date'], '%Y-%m-%d') - timedelta(days=1)
            except (ValueError, KeyError):
                return None
            vals = {
                'start_date': start_dt.strftime('%Y-%m-%d'),
                'stop_date': end_dt.strftime('%Y-%m-%d'),
                'allday': True,
            }
        else:
            try:
                start_str = start_data.get('dateTime', '')
                end_str = end_data.get('dateTime', '')
                start_dt = datetime.fromisoformat(start_str.replace('Z', '+00:00'))
                end_dt = datetime.fromisoformat(end_str.replace('Z', '+00:00'))
                # Convert to naive UTC for Odoo
                start_utc = start_dt.astimezone(tz=None).replace(tzinfo=None) if start_dt.tzinfo else start_dt
                end_utc = end_dt.astimezone(tz=None).replace(tzinfo=None) if end_dt.tzinfo else end_dt
            except (ValueError, KeyError):
                return None
            vals = {
                'start': start_utc,
                'stop': end_utc,
                'allday': False,
            }

        vals.update({
            'name': event_data.get('summary', '(No Title)'),
            'description': event_data.get('description', ''),
            'location': event_data.get('location', ''),
            'privacy': 'private' if event_data.get('visibility') == 'private' else 'public',
            'show_as': 'busy' if event_data.get('transparency', 'opaque') == 'opaque' else 'free',
            'x_fc_source_account_id': self.id,
        })
        return vals

    def _sync_pull_microsoft(self, token):
        """Pull events from Microsoft Calendar."""
        self.ensure_one()
        headers = {
            'Authorization': 'Bearer %s' % token,
            'Content-Type': 'application/json',
            'Prefer': 'odata.maxpagesize=50',
        }

        if self.x_fc_sync_token:
            url = '%s/me/calendarView/delta?$select=%s&$deltatoken=%s' % (
                MICROSOFT_GRAPH_API, MICROSOFT_SELECT_FIELDS, self.x_fc_sync_token,
            )
        else:
            start_dt = (datetime.utcnow() - timedelta(days=14)).strftime('%Y-%m-%dT00:00:00Z')
            end_dt = (datetime.utcnow() + timedelta(days=30)).strftime('%Y-%m-%dT23:59:59Z')
            url = '%s/me/calendarView/delta?$select=%s&startDateTime=%s&endDateTime=%s' % (
                MICROSOFT_GRAPH_API, MICROSOFT_SELECT_FIELDS, start_dt, end_dt,
            )

        all_events = []
        next_sync_token = self.x_fc_sync_token
        page_num = 0
        max_events = 5000 if self.x_fc_sync_token else 2000

        while url:
            page_num += 1
            _logger.warning("MS sync account %s page %d: fetching...", self.id, page_num)
            resp = self._microsoft_request_with_retry(url, headers)
            _logger.warning("MS sync account %s page %d: HTTP %s, %d bytes", self.id, page_num, resp.status_code, len(resp.content))

            if resp.status_code in (400, 410):
                error_data = {}
                try:
                    error_data = resp.json().get('error', {})
                except Exception:
                    pass
                error_code = error_data.get('code', '')
                if 'fullSyncRequired' in error_code or 'SyncStateNotFound' in error_code:
                    _logger.warning("Microsoft sync token expired for account %s, doing full sync", self.id)
                    self.sudo().x_fc_sync_token = False
                    return self._sync_pull_microsoft(token)
                resp.raise_for_status()

            resp.raise_for_status()
            data = resp.json()

            page_events = data.get('value', [])
            all_events.extend(page_events)
            _logger.warning("MS sync account %s page %d: %d events (total %d)", self.id, page_num, len(page_events), len(all_events))

            if len(all_events) >= max_events:
                _logger.warning(
                    "MS sync account %s: hit event limit (%d/%d), stopping fetch",
                    self.id, len(all_events), max_events,
                )
                break

            url = data.get('@odata.nextLink')
            if not url:
                delta_link = data.get('@odata.deltaLink', '')
                if '$deltatoken=' in delta_link:
                    next_sync_token = delta_link.split('$deltatoken=')[-1]

        _logger.warning("MS sync account %s: processing %d events...", self.id, len(all_events))
        created = 0
        updated = 0
        deleted = 0
        for i, event_data in enumerate(all_events):
            result = self._process_microsoft_event(event_data)
            if result == 'created':
                created += 1
            elif result == 'updated':
                updated += 1
            elif result == 'deleted':
                deleted += 1
            if (i + 1) % 25 == 0:
                _logger.warning("MS sync account %s: processed %d/%d events", self.id, i + 1, len(all_events))

        self.sudo().write({
            'x_fc_sync_token': next_sync_token,
            'x_fc_last_sync': fields.Datetime.now(),
            'x_fc_sync_status': 'active',
            'x_fc_error_message': False,
        })

        _logger.warning(
            "MS sync account %s (%s) DONE: %d created, %d updated, %d deleted",
            self.id, self.x_fc_email, created, updated, deleted,
        )
        return True

    def _microsoft_request_with_retry(self, url, headers):
        """GET request with automatic retry on 429 / 503 and connection errors."""
        last_exc = None
        for attempt in range(MAX_THROTTLE_RETRIES + 1):
            try:
                resp = requests.get(url, headers=headers, timeout=TIMEOUT)
            except (requests.ConnectionError, requests.Timeout) as e:
                last_exc = e
                wait = min(DEFAULT_RETRY_SECONDS * (attempt + 1), 30)
                _logger.warning(
                    "Microsoft connection error for account %s: %s (retry in %ds, attempt %d/%d)",
                    self.id, e, wait, attempt + 1, MAX_THROTTLE_RETRIES,
                )
                if attempt < MAX_THROTTLE_RETRIES:
                    time.sleep(wait)
                    continue
                raise
            if resp.status_code not in (429, 503):
                return resp
            retry_after = int(resp.headers.get('Retry-After', DEFAULT_RETRY_SECONDS))
            retry_after = min(retry_after, 60)
            _logger.warning(
                "Microsoft throttled account %s (HTTP %s), retry in %ds (attempt %d/%d)",
                self.id, resp.status_code, retry_after, attempt + 1, MAX_THROTTLE_RETRIES,
            )
            if attempt < MAX_THROTTLE_RETRIES:
                time.sleep(retry_after)
        return resp

    def _process_microsoft_event(self, event_data):
        """Process a single Microsoft Calendar event."""
        self.ensure_one()
        external_id = event_data.get('id')
        if not external_id:
            return None

        ctx = self._silent_ctx()
        EventLink = self.env['fusion.calendar.event.link'].sudo()
        CalendarEvent = self.env['calendar.event'].sudo().with_context(**ctx)

        link = EventLink.search([
            ('x_fc_account_id', '=', self.id),
            ('x_fc_external_id', '=', external_id),
        ], limit=1)

        if event_data.get('@removed') or event_data.get('isCancelled'):
            if link and link.x_fc_event_id:
                link.x_fc_event_id.with_context(**ctx).write({'active': False})
                link.unlink()
            return 'deleted'

        vals = self._microsoft_event_to_odoo_vals(event_data)
        if not vals:
            return None

        ical_uid = event_data.get('iCalUId', '')

        if link:
            if link.x_fc_event_id and link.x_fc_event_id.active:
                link.x_fc_event_id.with_context(**ctx).write(vals)
                link.write({'x_fc_last_synced': fields.Datetime.now()})
            return 'updated'

        existing_link = EventLink.search([
            ('x_fc_universal_id', '=', ical_uid),
            ('x_fc_universal_id', '!=', False),
        ], limit=1) if ical_uid else None

        if existing_link and existing_link.x_fc_event_id:
            self._upsert_event_link(EventLink, existing_link.x_fc_event_id.id, external_id, ical_uid)
            return 'updated'

        reuse_event = self._find_existing_event(CalendarEvent, vals)
        if reuse_event:
            self._upsert_event_link(EventLink, reuse_event.id, external_id, ical_uid)
            return 'updated'

        vals['partner_ids'] = [(4, self.x_fc_user_id.partner_id.id)]
        event = CalendarEvent.create(vals)
        self._upsert_event_link(EventLink, event.id, external_id, ical_uid)
        return 'created'

    def _microsoft_event_to_odoo_vals(self, event_data):
        """Convert Microsoft Graph API event dict to Odoo calendar.event vals."""
        start_data = event_data.get('start', {})
        end_data = event_data.get('end', {})
        if not start_data or not end_data:
            return None

        allday = event_data.get('isAllDay', False)

        try:
            if allday:
                start_dt = datetime.fromisoformat(start_data['dateTime'][:10])
                end_dt = datetime.fromisoformat(end_data['dateTime'][:10]) - timedelta(days=1)
                vals = {
                    'start_date': start_dt.strftime('%Y-%m-%d'),
                    'stop_date': end_dt.strftime('%Y-%m-%d'),
                    'allday': True,
                }
            else:
                # Microsoft returns dateTime in UTC with timeZone field
                start_str = start_data.get('dateTime', '')
                end_str = end_data.get('dateTime', '')
                # Remove fractional seconds if present and parse
                start_utc = datetime.fromisoformat(start_str.split('.')[0])
                end_utc = datetime.fromisoformat(end_str.split('.')[0])
                vals = {
                    'start': start_utc,
                    'stop': end_utc,
                    'allday': False,
                }
        except (ValueError, KeyError):
            return None

        subject = event_data.get('subject') or ''
        description = event_data.get('bodyPreview') or ''
        if not description:
            body = event_data.get('body', {})
            if body.get('contentType') == 'text':
                description = body.get('content', '')
        location_data = event_data.get('location', {})
        location = location_data.get('displayName', '') if location_data else ''
        sensitivity = event_data.get('sensitivity', 'normal')
        show_as = event_data.get('showAs', 'busy')

        if not subject:
            subject = self._fetch_microsoft_event_subject(event_data.get('id'))

        if not subject:
            subject = '(No Title)'

        vals.update({
            'name': subject,
            'description': description,
            'location': location,
            'privacy': 'private' if sensitivity == 'private' else 'public',
            'show_as': 'busy' if show_as in ('busy', 'tentative', 'oof') else 'free',
            'x_fc_source_account_id': self.id,
        })
        return vals

    def _fetch_microsoft_event_subject(self, event_id):
        """Fetch subject from the full event when delta response omits it."""
        if not event_id:
            return ''
        try:
            token = self.sudo().x_fc_token
            if not token:
                return ''
            resp = requests.get(
                '%s/me/events/%s?$select=subject' % (MICROSOFT_GRAPH_API, event_id),
                headers={'Authorization': 'Bearer %s' % token},
                timeout=TIMEOUT,
            )
            if resp.status_code == 200:
                return resp.json().get('subject', '')
        except Exception:
            pass
        return ''

    # -------------------------------------------------------------------------
    # Sync Engine — Push (Odoo → External)
    # -------------------------------------------------------------------------

    def _sync_push_event(self, event):
        """Push an Odoo calendar event to this external calendar."""
        self.ensure_one()
        token = self._get_valid_token()
        if not token:
            return False

        EventLink = self.env['fusion.calendar.event.link'].sudo()

        # Check if already linked
        link = EventLink.search([
            ('x_fc_event_id', '=', event.id),
            ('x_fc_account_id', '=', self.id),
        ], limit=1)

        try:
            if self.x_fc_provider == 'google':
                if link:
                    self._google_patch_event(link.x_fc_external_id, event, token)
                    link.write({'x_fc_last_synced': fields.Datetime.now()})
                else:
                    external_id, ical_uid = self._google_insert_event(event, token)
                    EventLink.create({
                        'x_fc_event_id': event.id,
                        'x_fc_account_id': self.id,
                        'x_fc_external_id': external_id,
                        'x_fc_universal_id': ical_uid,
                        'x_fc_sync_direction': 'push',
                        'x_fc_last_synced': fields.Datetime.now(),
                    })
            elif self.x_fc_provider == 'microsoft':
                if link:
                    self._microsoft_patch_event(link.x_fc_external_id, event, token)
                    link.write({'x_fc_last_synced': fields.Datetime.now()})
                else:
                    external_id, ical_uid = self._microsoft_insert_event(event, token)
                    EventLink.create({
                        'x_fc_event_id': event.id,
                        'x_fc_account_id': self.id,
                        'x_fc_external_id': external_id,
                        'x_fc_universal_id': ical_uid,
                        'x_fc_sync_direction': 'push',
                        'x_fc_last_synced': fields.Datetime.now(),
                    })
            return True
        except Exception as e:
            _logger.warning("Failed to push event %s to account %s: %s", event.id, self.id, e)
            return False

    def _google_insert_event(self, event, token):
        """Create a new event on Google Calendar."""
        calendar_id = self.x_fc_calendar_id or 'primary'
        url = '%s/calendars/%s/events' % (GOOGLE_CALENDAR_API, calendar_id)
        values = self._odoo_event_to_google(event)
        resp = requests.post(
            url, json=values,
            headers={'Authorization': 'Bearer %s' % token},
            timeout=TIMEOUT,
        )
        resp.raise_for_status()
        data = resp.json()
        return data.get('id', ''), data.get('iCalUID', '')

    def _google_patch_event(self, external_id, event, token):
        """Update an existing event on Google Calendar."""
        calendar_id = self.x_fc_calendar_id or 'primary'
        url = '%s/calendars/%s/events/%s' % (GOOGLE_CALENDAR_API, calendar_id, external_id)
        values = self._odoo_event_to_google(event)
        resp = requests.patch(
            url, json=values,
            headers={'Authorization': 'Bearer %s' % token},
            timeout=TIMEOUT,
        )
        resp.raise_for_status()

    def _google_delete_event(self, external_id, token):
        """Delete an event from Google Calendar."""
        calendar_id = self.x_fc_calendar_id or 'primary'
        url = '%s/calendars/%s/events/%s' % (GOOGLE_CALENDAR_API, calendar_id, external_id)
        resp = requests.delete(
            url,
            headers={'Authorization': 'Bearer %s' % token},
            timeout=TIMEOUT,
        )
        if resp.status_code not in (204, 404, 410):
            resp.raise_for_status()

    def _microsoft_insert_event(self, event, token):
        """Create a new event on Microsoft Calendar."""
        url = '%s/me/events' % MICROSOFT_GRAPH_API
        values = self._odoo_event_to_microsoft(event)
        resp = requests.post(
            url, json=values,
            headers={
                'Authorization': 'Bearer %s' % token,
                'Content-Type': 'application/json',
            },
            timeout=TIMEOUT,
        )
        resp.raise_for_status()
        data = resp.json()
        return data.get('id', ''), data.get('iCalUId', '')

    def _microsoft_patch_event(self, external_id, event, token):
        """Update an existing event on Microsoft Calendar."""
        url = '%s/me/events/%s' % (MICROSOFT_GRAPH_API, external_id)
        values = self._odoo_event_to_microsoft(event)
        resp = requests.patch(
            url, json=values,
            headers={
                'Authorization': 'Bearer %s' % token,
                'Content-Type': 'application/json',
            },
            timeout=TIMEOUT,
        )
        resp.raise_for_status()

    def _microsoft_delete_event(self, external_id, token):
        """Delete an event from Microsoft Calendar."""
        url = '%s/me/events/%s' % (MICROSOFT_GRAPH_API, external_id)
        resp = requests.delete(
            url,
            headers={'Authorization': 'Bearer %s' % token},
            timeout=TIMEOUT,
        )
        if resp.status_code not in (204, 404):
            resp.raise_for_status()

    # -------------------------------------------------------------------------
    # Event Format Conversion (Odoo → External)
    # -------------------------------------------------------------------------

    def _odoo_event_to_google(self, event):
        """Convert Odoo calendar.event to Google Calendar API format."""
        values = {
            'summary': event.name or '',
            'description': event.description or '',
            'location': event.location or '',
        }
        if event.allday:
            values['start'] = {'date': event.start_date.strftime('%Y-%m-%d')}
            # Google end date is exclusive
            values['end'] = {'date': (event.stop_date + timedelta(days=1)).strftime('%Y-%m-%d')}
        else:
            values['start'] = {'dateTime': event.start.strftime('%Y-%m-%dT%H:%M:%S'), 'timeZone': 'UTC'}
            values['end'] = {'dateTime': event.stop.strftime('%Y-%m-%dT%H:%M:%S'), 'timeZone': 'UTC'}

        if event.privacy == 'private':
            values['visibility'] = 'private'
        if hasattr(event, 'show_as') and event.show_as == 'free':
            values['transparency'] = 'transparent'
        else:
            values['transparency'] = 'opaque'

        return values

    def _odoo_event_to_microsoft(self, event):
        """Convert Odoo calendar.event to Microsoft Graph API format."""
        values = {
            'subject': event.name or '',
            'body': {
                'contentType': 'text',
                'content': event.description or '',
            },
        }
        if event.location:
            values['location'] = {'displayName': event.location}

        if event.allday:
            values['isAllDay'] = True
            values['start'] = {
                'dateTime': event.start_date.strftime('%Y-%m-%dT00:00:00'),
                'timeZone': 'UTC',
            }
            values['end'] = {
                'dateTime': (event.stop_date + timedelta(days=1)).strftime('%Y-%m-%dT00:00:00'),
                'timeZone': 'UTC',
            }
        else:
            values['start'] = {
                'dateTime': event.start.strftime('%Y-%m-%dT%H:%M:%S'),
                'timeZone': 'UTC',
            }
            values['end'] = {
                'dateTime': event.stop.strftime('%Y-%m-%dT%H:%M:%S'),
                'timeZone': 'UTC',
            }

        if event.privacy == 'private':
            values['sensitivity'] = 'private'
        if hasattr(event, 'show_as') and event.show_as == 'free':
            values['showAs'] = 'free'
        else:
            values['showAs'] = 'busy'

        return values

    # -------------------------------------------------------------------------
    # Cross-Calendar Busy Blocking
    # -------------------------------------------------------------------------

    def _cross_calendar_push(self):
        """Push Odoo-native events to the FIRST active external calendar only.

        Only pushes events that were created in Odoo (not pulled from any
        external calendar) to avoid a pull->push->pull feedback loop.
        Pushes to a single calendar to prevent duplicates across calendars.
        """
        self.ensure_one()
        user = self.x_fc_user_id
        all_accounts = self.sudo().search([
            ('x_fc_user_id', '=', user.id),
            ('x_fc_active', '=', True),
            ('x_fc_sync_status', '=', 'active'),
        ], order='id ASC')
        if not all_accounts:
            return

        target_account = all_accounts[0]

        EventLink = self.env['fusion.calendar.event.link'].sudo()
        events = self.env['calendar.event'].sudo().search([
            ('partner_ids', 'in', [user.partner_id.id]),
            ('start', '>=', fields.Datetime.now() - timedelta(days=1)),
            ('start', '<=', fields.Datetime.now() + timedelta(days=90)),
            ('active', '=', True),
        ])

        for event in events:
            existing_links = EventLink.search([('x_fc_event_id', '=', event.id)])
            if existing_links:
                continue
            try:
                target_account._sync_push_event(event)
            except Exception as e:
                _logger.warning(
                    "Failed cross-calendar push of event %s to account %s: %s",
                    event.id, target_account.id, e,
                )

    # -------------------------------------------------------------------------
    # Backend RPC Methods
    # -------------------------------------------------------------------------

    @api.model
    def get_user_accounts_status(self):
        """Return connected calendar accounts for the current user (called from backend calendar)."""
        accounts = self.sudo().search([
            ('x_fc_user_id', '=', self.env.user.id),
            ('x_fc_active', '=', True),
        ])
        return [{
            'id': a.id,
            'email': a.x_fc_email,
            'provider': a.x_fc_provider,
            'status': a.x_fc_sync_status,
            'last_sync': str(a.x_fc_last_sync) if a.x_fc_last_sync else False,
        } for a in accounts]

    @api.model
    def sync_current_user(self):
        """Trigger immediate sync for the current user's accounts (called from backend calendar)."""
        accounts = self.sudo().search([
            ('x_fc_user_id', '=', self.env.user.id),
            ('x_fc_active', '=', True),
            ('x_fc_sync_status', 'in', ['active', 'error']),
            ('x_fc_rtoken', '!=', False),
        ])
        if not accounts:
            return {'success': False, 'error': 'No connected calendar accounts found.'}

        synced = 0
        errors = []
        for account in accounts:
            try:
                account._sync_pull()
                self.env.cr.commit()
                synced += 1
            except Exception as e:
                _logger.exception("Manual sync error for account %s: %s", account.id, e)
                self.env.cr.rollback()
                errors.append('%s: %s' % (account.x_fc_email, str(e)[:100]))

        if errors:
            return {
                'success': synced > 0,
                'message': 'Synced %d account(s). Errors: %s' % (synced, '; '.join(errors)),
            }
        return {'success': True, 'message': 'Synced %d account(s) successfully.' % synced}

    # -------------------------------------------------------------------------
    # Cron Job
    # -------------------------------------------------------------------------

    @api.model
    def _cron_sync_all_accounts(self):
        """Cron entry point: sync all active accounts."""
        all_accounts = self.sudo().search([
            ('x_fc_active', '=', True),
            ('x_fc_sync_status', 'in', ['active', 'error']),
            ('x_fc_rtoken', '!=', False),
        ])
        never_synced = all_accounts.filtered(lambda a: not a.x_fc_last_sync)
        previously_synced = all_accounts - never_synced
        accounts = never_synced + previously_synced

        users_synced = set()

        for account in accounts:
            try:
                account._sync_pull()
                self.env.cr.commit()
                users_synced.add(account.x_fc_user_id.id)
            except Exception as e:
                _logger.exception("Cron sync error for account %s: %s", account.id, e)
                self.env.cr.rollback()
                try:
                    account.sudo().write({
                        'x_fc_sync_status': 'error',
                        'x_fc_error_message': str(e)[:500],
                    })
                    self.env.cr.commit()
                except Exception:
                    self.env.cr.rollback()

        # Cross-calendar push for each user
        for user_id in users_synced:
            user_accounts = accounts.filtered(lambda a: a.x_fc_user_id.id == user_id)
            if len(user_accounts) > 1:
                try:
                    user_accounts[0]._cross_calendar_push()
                    self.env.cr.commit()
                except Exception as e:
                    _logger.exception("Cross-calendar push error for user %s: %s", user_id, e)
                    self.env.cr.rollback()

    # -------------------------------------------------------------------------
    # Delete External Events on Account Disconnect
    # -------------------------------------------------------------------------

    def action_disconnect(self):
        """Disconnect this calendar account and clean up."""
        self.ensure_one()
        # Remove all pushed events from external calendar
        pushed_links = self.x_fc_link_ids.filtered(lambda l: l.x_fc_sync_direction == 'push')
        token = False
        try:
            token = self._get_valid_token()
        except Exception:
            pass

        if token:
            for link in pushed_links:
                try:
                    if self.x_fc_provider == 'google':
                        self._google_delete_event(link.x_fc_external_id, token)
                    elif self.x_fc_provider == 'microsoft':
                        self._microsoft_delete_event(link.x_fc_external_id, token)
                except Exception as e:
                    _logger.warning("Failed to delete external event %s: %s", link.x_fc_external_id, e)

        # Clean up
        self.x_fc_link_ids.unlink()
        self.write({
            'x_fc_active': False,
            'x_fc_rtoken': False,
            'x_fc_token': False,
            'x_fc_sync_token': False,
            'x_fc_sync_status': 'paused',
        })
