<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Service;

use OCA\DAV\CalDAV\CalDavBackend;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCP\App\IAppManager;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCalendar;

/**
 * Syncs member birthday events to a dedicated Nextcloud calendar via CalDAV.
 *
 * Creates/manages the "Pfadfinder Geburtstage" calendar with recurring yearly
 * all-day events for each active member (excluding Erziehungsberechtigte).
 *
 * Uses CalDavBackend for direct CalDAV writes (user-triggered sync) and
 * a staging table (mv_calendar_events) for background job processing.
 * All sync operations are guarded by PermissionService to enforce Nextcloud
 * access control. See Issue #170.
 *
 * Part of Issue #50.
 */
class CalendarSyncService {

    private const CALENDAR_URI = 'pfadfinder-geburtstage';
    private const CALENDAR_NAME = 'Pfadfinder Geburtstage';

    private MemberMapper $memberMapper;
    private IDBConnection $db;
    private PermissionService $permissionService;
    private IUserSession $userSession;
    private LoggerInterface $logger;
    private CalDavBackend $caldavBackend;
    private IAppManager $appManager;

    public function __construct(
        MemberMapper $memberMapper,
        IDBConnection $db,
        PermissionService $permissionService,
        IUserSession $userSession,
        LoggerInterface $logger,
        CalDavBackend $caldavBackend,
        IAppManager $appManager
    ) {
        $this->memberMapper = $memberMapper;
        $this->db = $db;
        $this->permissionService = $permissionService;
        $this->userSession = $userSession;
        $this->logger = $logger;
        $this->caldavBackend = $caldavBackend;
        $this->appManager = $appManager;
    }

    /**
     * Assert that the current user has write-level access.
     *
     * Sync operations modify data (calendar events linked to members),
     * so at minimum write permission is required.
     *
     * In background job context (no user session), this check is skipped
     * as background jobs are system-level operations.
     *
     * @throws \RuntimeException if a user is logged in but lacks permission
     */
    private function assertWritePermission(): void {
        $user = $this->userSession->getUser();
        if ($user === null) {
            // Background job context — no user session available.
            // Background jobs are trusted system-level operations.
            $this->logger->debug('Calendar sync running in background context (no user session)', [
                'app' => 'mitgliederverwaltung',
            ]);
            return;
        }

        $userId = $user->getUID();
        if (!$this->permissionService->canWrite($userId)) {
            $this->logger->warning('Calendar sync denied: insufficient permissions', [
                'userId' => $userId,
                'app' => 'mitgliederverwaltung',
            ]);
            throw new \RuntimeException('Calendar sync requires write permission');
        }
    }

    // ── CalDAV backend operations ───────────────────────────────────

    /**
     * Get the status of calendar sync for a user.
     *
     * @return array{calendarAppEnabled: bool, calendarExists: bool, calendarId: ?int, eventCount: int}
     */
    public function getCalendarStatus(string $userId): array {
        $calendarAppEnabled = $this->appManager->isEnabledForUser('calendar');

        $calendarId = $this->findCalendarId($userId);
        $eventCount = 0;

        if ($calendarId !== null) {
            $objects = $this->caldavBackend->getCalendarObjects($calendarId);
            $eventCount = count($objects);
        }

        // Count members that should have events
        $members = $this->memberMapper->findAll();
        $eligibleCount = 0;
        foreach ($members as $member) {
            if ($this->shouldHaveEvent($member)) {
                $eligibleCount++;
            }
        }

        return [
            'calendarAppEnabled' => $calendarAppEnabled,
            'calendarExists' => $calendarId !== null,
            'calendarId' => $calendarId,
            'eventCount' => $eventCount,
            'eligibleMembers' => $eligibleCount,
        ];
    }

    /**
     * Check if the Calendar UI app is enabled.
     */
    public function isCalendarAppEnabled(): bool {
        return $this->appManager->isEnabledForUser('calendar');
    }

    /**
     * Enable the Calendar UI app.
     *
     * @return bool true if successfully enabled, false if the app is not installed
     */
    public function enableCalendarApp(): bool {
        try {
            $this->appManager->enableApp('calendar');
            return true;
        } catch (\Exception $e) {
            $this->logger->warning('Failed to enable calendar app', [
                'exception' => $e,
                'app' => 'mitgliederverwaltung',
            ]);
            return false;
        }
    }

    /**
     * Find the calendar ID for our dedicated calendar.
     */
    private function findCalendarId(string $userId): ?int {
        $principalUri = 'principals/users/' . $userId;
        $calendars = $this->caldavBackend->getCalendarsForUser($principalUri);

        foreach ($calendars as $calendar) {
            if ($calendar['uri'] === self::CALENDAR_URI) {
                return (int)$calendar['id'];
            }
        }

        return null;
    }

    /**
     * Ensure the dedicated calendar exists, creating it if needed.
     *
     * @return int The calendar ID
     */
    public function ensureCalendarExists(string $userId): int {
        $calendarId = $this->findCalendarId($userId);
        if ($calendarId !== null) {
            return $calendarId;
        }

        $principalUri = 'principals/users/' . $userId;
        $calendarId = $this->caldavBackend->createCalendar($principalUri, self::CALENDAR_URI, [
            '{DAV:}displayname' => self::CALENDAR_NAME,
            '{urn:ietf:params:xml:ns:caldav}calendar-description' =>
                'Automatisch verwaltete Geburtstage und Erinnerungen der Pfadfinder-Mitglieder',
            '{http://apple.com/ns/ical/}calendar-color' => '#4CAF50FF',
        ]);

        $this->logger->info('Created calendar for user', [
            'userId' => $userId,
            'calendarId' => $calendarId,
            'app' => 'mitgliederverwaltung',
        ]);

        return (int)$calendarId;
    }

    /**
     * Full sync to the real CalDAV backend for a specific user.
     *
     * Creates the calendar if it doesn't exist, then creates/updates/deletes
     * all birthday events and Juleica reminders.
     *
     * @return array{created: int, updated: int, deleted: int, juleica: int}
     */
    public function fullSyncCalDav(string $userId): array {
        $this->assertWritePermission();

        $stats = ['created' => 0, 'updated' => 0, 'deleted' => 0, 'juleica' => 0];

        $calendarId = $this->ensureCalendarExists($userId);
        $members = $this->memberMapper->findAll();

        // Track valid event URIs to clean up orphans later
        $validUris = [];

        foreach ($members as $member) {
            $shouldHaveEvent = $this->shouldHaveEvent($member);
            $eventUri = $this->generateEventUri($member);
            $hasEvent = $this->caldavObjectExists($calendarId, $eventUri);

            if ($shouldHaveEvent && !$hasEvent) {
                $vcalendar = $this->buildVEvent($member);
                $this->caldavBackend->createCalendarObject($calendarId, $eventUri, $vcalendar);
                $this->updateMemberCalendarUri($member->getId(), $eventUri);
                $this->storeCalendarEvent($eventUri, $vcalendar);
                $stats['created']++;
                $validUris[] = $eventUri;
            } elseif ($shouldHaveEvent && $hasEvent) {
                $vcalendar = $this->buildVEvent($member);
                $this->caldavBackend->updateCalendarObject($calendarId, $eventUri, $vcalendar);
                $this->storeCalendarEvent($eventUri, $vcalendar);
                $stats['updated']++;
                $validUris[] = $eventUri;
            } elseif (!$shouldHaveEvent && $hasEvent) {
                $this->caldavBackend->deleteCalendarObject($calendarId, $eventUri);
                $this->updateMemberCalendarUri($member->getId(), null);
                $this->removeCalendarEvent($eventUri);
                $stats['deleted']++;
            }

            // Juleica reminder
            $hasJuleica = $member->getJuleicaAblaufdatum() !== null
                && $member->getJuleicaAblaufdatum() !== ''
                && $member->getDeletedAt() === null;
            $juleicaUri = $this->generateJuleicaEventUri($member);
            $juleicaExists = $this->caldavObjectExists($calendarId, $juleicaUri);

            if ($hasJuleica) {
                $juleicaData = $this->buildJuleicaVEvent($member);
                if ($juleicaExists) {
                    $this->caldavBackend->updateCalendarObject($calendarId, $juleicaUri, $juleicaData);
                } else {
                    $this->caldavBackend->createCalendarObject($calendarId, $juleicaUri, $juleicaData);
                }
                $this->storeCalendarEvent($juleicaUri, $juleicaData);
                $stats['juleica']++;
                $validUris[] = $juleicaUri;
            } elseif ($juleicaExists) {
                $this->caldavBackend->deleteCalendarObject($calendarId, $juleicaUri);
                $this->removeCalendarEvent($juleicaUri);
            }
        }

        // Clean up orphaned events (events with mv- prefix that don't match any member)
        $allCalObjects = $this->caldavBackend->getCalendarObjects($calendarId);
        foreach ($allCalObjects as $obj) {
            $uri = $obj['uri'];
            if (str_starts_with($uri, 'mv-') && !in_array($uri, $validUris, true)) {
                $this->caldavBackend->deleteCalendarObject($calendarId, $uri);
                $this->removeCalendarEvent($uri);
                $stats['deleted']++;
            }
        }

        $this->logger->info('Calendar CalDAV full sync completed', [
            'userId' => $userId,
            'created' => $stats['created'],
            'updated' => $stats['updated'],
            'deleted' => $stats['deleted'],
            'juleica' => $stats['juleica'],
            'app' => 'mitgliederverwaltung',
        ]);

        return $stats;
    }

    /**
     * Check if a CalDAV object exists in the calendar.
     */
    private function caldavObjectExists(int $calendarId, string $objectUri): bool {
        $obj = $this->caldavBackend->getCalendarObject($calendarId, $objectUri);
        return $obj !== null;
    }

    // ── Staging table operations (for background jobs) ──────────────

    /**
     * Sync a single member's birthday event (staging table).
     *
     * Creates, updates, or deletes the calendar event based on member state:
     * - Active member with role != Erziehungsberechtigter => create/update event
     * - Inactive or Erziehungsberechtigter => delete event
     *
     * @param Member $member The member to sync
     */
    public function syncMember(Member $member): void {
        $this->assertWritePermission();

        $shouldHaveEvent = $this->shouldHaveEvent($member);
        $hasEvent = $member->getCalendarEventUri() !== null;

        if ($shouldHaveEvent && !$hasEvent) {
            $this->createEvent($member);
        } elseif ($shouldHaveEvent && $hasEvent) {
            $this->updateEvent($member);
        } elseif (!$shouldHaveEvent && $hasEvent) {
            $this->deleteEvent($member);
        }
    }

    /**
     * Full reconciliation sync: compare all members with calendar events.
     *
     * Catches drift from missed queue events.
     *
     * @return array{created: int, updated: int, deleted: int}
     */
    public function fullSync(): array {
        $this->assertWritePermission();

        $stats = ['created' => 0, 'updated' => 0, 'deleted' => 0];

        try {
            $members = $this->memberMapper->findAll();

            foreach ($members as $member) {
                $shouldHaveEvent = $this->shouldHaveEvent($member);
                $hasEvent = $member->getCalendarEventUri() !== null;

                if ($shouldHaveEvent && !$hasEvent) {
                    $this->createEvent($member);
                    $stats['created']++;
                } elseif ($shouldHaveEvent && $hasEvent) {
                    $this->updateEvent($member);
                    $stats['updated']++;
                } elseif (!$shouldHaveEvent && $hasEvent) {
                    $this->deleteEvent($member);
                    $stats['deleted']++;
                }
            }

            $this->logger->info('Calendar full sync completed', [
                'created' => $stats['created'],
                'updated' => $stats['updated'],
                'deleted' => $stats['deleted'],
                'app' => 'mitgliederverwaltung',
            ]);
        } catch (\Exception $e) {
            $this->logger->error('Calendar full sync failed', [
                'exception' => $e,
                'app' => 'mitgliederverwaltung',
            ]);
        }

        return $stats;
    }

    /**
     * Enqueue a member for calendar sync.
     * Processed by the SyncQueueJob background task.
     */
    public function enqueueSync(int $memberId): void {
        $qb = $this->db->getQueryBuilder();
        $qb->insert('mv_sync_queue')
            ->values([
                'entity_type' => $qb->createNamedParameter('calendar'),
                'entity_id' => $qb->createNamedParameter($memberId, IQueryBuilder::PARAM_INT),
                'created_at' => $qb->createNamedParameter(
                    (new \DateTime())->format('Y-m-d H:i:s')
                ),
            ]);

        try {
            $qb->executeStatement();
        } catch (\Exception $e) {
            $this->logger->warning('Failed to enqueue calendar sync', [
                'memberId' => $memberId,
                'exception' => $e,
                'app' => 'mitgliederverwaltung',
            ]);
        }
    }

    /**
     * Process pending sync queue entries.
     *
     * @return int Number of entries processed
     */
    public function processQueue(): int {
        $qb = $this->db->getQueryBuilder();
        $qb->select('*')
            ->from('mv_sync_queue')
            ->where($qb->expr()->eq('entity_type', $qb->createNamedParameter('calendar')))
            ->orderBy('created_at', 'ASC')
            ->setMaxResults(100);

        $result = $qb->executeQuery();
        $processed = 0;

        while ($row = $result->fetch()) {
            try {
                $member = $this->memberMapper->findById((int)$row['entity_id']);
                $this->syncMember($member);
                $processed++;
            } catch (\Exception $e) {
                $this->logger->warning('Failed to sync member from queue', [
                    'entityId' => $row['entity_id'],
                    'exception' => $e,
                    'app' => 'mitgliederverwaltung',
                ]);
            }

            // Delete processed entry
            $deleteQb = $this->db->getQueryBuilder();
            $deleteQb->delete('mv_sync_queue')
                ->where($deleteQb->expr()->eq('id', $deleteQb->createNamedParameter(
                    (int)$row['id'],
                    IQueryBuilder::PARAM_INT
                )));
            $deleteQb->executeStatement();
        }

        $result->closeCursor();
        return $processed;
    }

    /**
     * Create a birthday calendar event for a member.
     */
    private function createEvent(Member $member): void {
        $eventUri = $this->generateEventUri($member);
        $vcalendar = $this->buildVEvent($member);

        // Store the event URI on the member for efficient updates
        $this->updateMemberCalendarUri($member->getId(), $eventUri);

        // Store event data for CalDAV sync
        $this->storeCalendarEvent($eventUri, $vcalendar);

        $this->logger->debug('Created calendar event', [
            'memberId' => $member->getId(),
            'eventUri' => $eventUri,
            'app' => 'mitgliederverwaltung',
        ]);
    }

    /**
     * Update an existing birthday calendar event.
     */
    private function updateEvent(Member $member): void {
        $eventUri = $member->getCalendarEventUri();
        $vcalendar = $this->buildVEvent($member);

        $this->storeCalendarEvent($eventUri, $vcalendar);

        $this->logger->debug('Updated calendar event', [
            'memberId' => $member->getId(),
            'eventUri' => $eventUri,
            'app' => 'mitgliederverwaltung',
        ]);
    }

    /**
     * Delete a birthday calendar event for a member.
     */
    public function deleteEvent(Member $member): void {
        $this->assertWritePermission();

        $eventUri = $member->getCalendarEventUri();

        if ($eventUri !== null) {
            $this->removeCalendarEvent($eventUri);
            $this->updateMemberCalendarUri($member->getId(), null);

            $this->logger->debug('Deleted calendar event', [
                'memberId' => $member->getId(),
                'eventUri' => $eventUri,
                'app' => 'mitgliederverwaltung',
            ]);
        }
    }

    // ── Helpers ──────────────────────────────────────────────────────

    /**
     * Determine if a member should have a birthday calendar event.
     */
    private function shouldHaveEvent(Member $member): bool {
        // Only active members who are not Erziehungsberechtigte and have a birthday
        return $member->getStatus() === 'aktiv'
            && strtolower($member->getRolle()) !== 'erziehungsberechtigter'
            && $member->getDeletedAt() === null
            && $member->getGeburtsdatum() !== null;
    }

    /**
     * Build a VCalendar event for a member's birthday.
     */
    private function buildVEvent(Member $member): string {
        $birthYear = substr($member->getGeburtsdatum(), 0, 4);

        $summary = sprintf(
            'Geburtstag: %s %s (*%s)',
            $member->getVorname(),
            $member->getNachname(),
            $birthYear
        );

        $vcal = new VCalendar();
        $vevent = $vcal->add('VEVENT', [
            'SUMMARY' => $summary,
            'DTSTART' => new \DateTime($member->getGeburtsdatum()),
            'RRULE' => 'FREQ=YEARLY',
            'TRANSP' => 'TRANSPARENT',
        ]);

        // All-day event
        $vevent->DTSTART['VALUE'] = 'DATE';

        return $vcal->serialize();
    }

    /**
     * Generate a unique event URI for a member.
     */
    private function generateEventUri(Member $member): string {
        return 'mv-birthday-' . $member->getId() . '.ics';
    }

    /**
     * Store or update a calendar event in the sync table.
     */
    private function storeCalendarEvent(string $eventUri, string $vcalendarData): void {
        // Check if exists
        $qb = $this->db->getQueryBuilder();
        $qb->select('id')
            ->from('mv_calendar_events')
            ->where($qb->expr()->eq('event_uri', $qb->createNamedParameter($eventUri)));

        $result = $qb->executeQuery();
        $existing = $result->fetch();
        $result->closeCursor();

        if ($existing) {
            $updateQb = $this->db->getQueryBuilder();
            $updateQb->update('mv_calendar_events')
                ->set('vcalendar_data', $updateQb->createNamedParameter($vcalendarData))
                ->set('updated_at', $updateQb->createNamedParameter(
                    (new \DateTime())->format('Y-m-d H:i:s')
                ))
                ->where($updateQb->expr()->eq('event_uri', $updateQb->createNamedParameter($eventUri)));
            $updateQb->executeStatement();
        } else {
            $insertQb = $this->db->getQueryBuilder();
            $insertQb->insert('mv_calendar_events')
                ->values([
                    'event_uri' => $insertQb->createNamedParameter($eventUri),
                    'vcalendar_data' => $insertQb->createNamedParameter($vcalendarData),
                    'calendar_uri' => $insertQb->createNamedParameter(self::CALENDAR_URI),
                    'created_at' => $insertQb->createNamedParameter(
                        (new \DateTime())->format('Y-m-d H:i:s')
                    ),
                    'updated_at' => $insertQb->createNamedParameter(
                        (new \DateTime())->format('Y-m-d H:i:s')
                    ),
                ]);
            $insertQb->executeStatement();
        }
    }

    /**
     * Remove a calendar event from the sync table.
     */
    private function removeCalendarEvent(string $eventUri): void {
        $qb = $this->db->getQueryBuilder();
        $qb->delete('mv_calendar_events')
            ->where($qb->expr()->eq('event_uri', $qb->createNamedParameter($eventUri)));
        $qb->executeStatement();
    }

    /**
     * Update a member's calendar_event_uri field.
     */
    private function updateMemberCalendarUri(int $memberId, ?string $uri): void {
        $qb = $this->db->getQueryBuilder();
        $qb->update('mv_members')
            ->set('calendar_event_uri', $qb->createNamedParameter($uri))
            ->where($qb->expr()->eq('id', $qb->createNamedParameter($memberId, IQueryBuilder::PARAM_INT)));
        $qb->executeStatement();
    }

    // ── Juleica reminder sync (Issue #160) ──────────────────────────

    /**
     * Sync a single member's Juleica expiry reminder.
     *
     * Creates a one-time reminder 2 months before juleica_ablaufdatum.
     * Removes the reminder if the date is cleared.
     * Updates the reminder if the date changes.
     *
     * @param Member $member The member to sync
     */
    public function syncJuleicaReminder(Member $member): void {
        $this->assertWritePermission();

        $juleicaUri = $this->generateJuleicaEventUri($member);
        $hasExpiry = $member->getJuleicaAblaufdatum() !== null
            && $member->getJuleicaAblaufdatum() !== '';
        $hasEvent = $this->juleicaEventExists($juleicaUri);

        if ($hasExpiry && $member->getDeletedAt() === null) {
            $vcalendar = $this->buildJuleicaVEvent($member);
            $this->storeCalendarEvent($juleicaUri, $vcalendar);

            $this->logger->debug('Synced Juleica reminder', [
                'memberId' => $member->getId(),
                'eventUri' => $juleicaUri,
                'app' => 'mitgliederverwaltung',
            ]);
        } elseif ($hasEvent) {
            $this->removeCalendarEvent($juleicaUri);

            $this->logger->debug('Removed Juleica reminder', [
                'memberId' => $member->getId(),
                'eventUri' => $juleicaUri,
                'app' => 'mitgliederverwaltung',
            ]);
        }
    }

    /**
     * Build a VCalendar event for a member's Juleica expiry reminder.
     * The event is placed 2 months before the Ablaufdatum.
     */
    private function buildJuleicaVEvent(Member $member): string {
        $expiry = new \DateTime($member->getJuleicaAblaufdatum());
        $reminderDate = (clone $expiry)->modify('-2 months');

        $summary = sprintf(
            'Juleica-Erneuerung: %s %s',
            $member->getVorname(),
            $member->getNachname()
        );

        $description = sprintf(
            'Die Juleica von %s %s laeuft am %s ab. Bitte rechtzeitig die Erneuerung einleiten.',
            $member->getVorname(),
            $member->getNachname(),
            $expiry->format('d.m.Y')
        );

        if ($member->getJuleicaNummer()) {
            $description .= "\nJuleica-Nummer: " . $member->getJuleicaNummer();
        }

        $vcal = new VCalendar();
        $vevent = $vcal->add('VEVENT', [
            'SUMMARY' => $summary,
            'DESCRIPTION' => $description,
            'DTSTART' => $reminderDate,
            'TRANSP' => 'TRANSPARENT',
        ]);

        // All-day event (non-recurring)
        $vevent->DTSTART['VALUE'] = 'DATE';

        return $vcal->serialize();
    }

    /**
     * Generate a unique event URI for a member's Juleica reminder.
     */
    private function generateJuleicaEventUri(Member $member): string {
        return 'mv-juleica-' . $member->getId() . '.ics';
    }

    /**
     * Check if a Juleica reminder event exists in the calendar staging table.
     */
    private function juleicaEventExists(string $eventUri): bool {
        $qb = $this->db->getQueryBuilder();
        $qb->select('id')
            ->from('mv_calendar_events')
            ->where($qb->expr()->eq('event_uri', $qb->createNamedParameter($eventUri)));

        $result = $qb->executeQuery();
        $exists = $result->fetch() !== false;
        $result->closeCursor();

        return $exists;
    }
}
