<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Service;

use OCA\Mitgliederverwaltung\Db\AddressMapper;
use OCA\Mitgliederverwaltung\Db\EmailMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCP\Contacts\IManager as IContactsManager;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCard;

/**
 * Syncs member contact data to a dedicated Nextcloud address book via CardDAV.
 *
 * Creates/manages the "Pfadfinder Mitglieder" address book with VCards
 * for all active members (including Erziehungsberechtigte).
 *
 * Uses OCP\Contacts\IManager for Nextcloud-native contacts integration
 * with proper access control. Falls back to the staging table
 * (mv_contact_cards) when the Contacts app is not available.
 * See Issue #170.
 *
 * Part of Issue #51.
 */
class ContactsSyncService {

    private const ADDRESSBOOK_URI = 'pfadfinder-mitglieder';
    private const ADDRESSBOOK_NAME = 'Pfadfinder Mitglieder';

    private MemberMapper $memberMapper;
    private AddressMapper $addressMapper;
    private PhoneMapper $phoneMapper;
    private EmailMapper $emailMapper;
    private IContactsManager $contactsManager;
    private PermissionService $permissionService;
    private IUserSession $userSession;
    private IDBConnection $db;
    private LoggerInterface $logger;

    public function __construct(
        MemberMapper $memberMapper,
        AddressMapper $addressMapper,
        PhoneMapper $phoneMapper,
        EmailMapper $emailMapper,
        IContactsManager $contactsManager,
        PermissionService $permissionService,
        IUserSession $userSession,
        IDBConnection $db,
        LoggerInterface $logger
    ) {
        $this->memberMapper = $memberMapper;
        $this->addressMapper = $addressMapper;
        $this->phoneMapper = $phoneMapper;
        $this->emailMapper = $emailMapper;
        $this->contactsManager = $contactsManager;
        $this->permissionService = $permissionService;
        $this->userSession = $userSession;
        $this->db = $db;
        $this->logger = $logger;
    }

    /**
     * Assert that the current user has write-level access.
     *
     * Sync operations modify contact data 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('Contacts sync running in background context (no user session)', [
                'app' => 'mitgliederverwaltung',
            ]);
            return;
        }

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

    /**
     * Find the Nextcloud address book key for our dedicated address book.
     *
     * @return string|null The address book key, or null if not found
     */
    private function findAddressBookKey(): ?string {
        if (!$this->contactsManager->isEnabled()) {
            return null;
        }

        $addressBooks = $this->contactsManager->getUserAddressBooks();
        foreach ($addressBooks as $book) {
            if ($book->getUri() === self::ADDRESSBOOK_URI) {
                return $book->getKey();
            }
        }

        return null;
    }

    /**
     * Sync a single member's VCard.
     *
     * Creates, updates, or deletes the contact based on member state:
     * - Active member (any role) => create/update VCard
     * - Inactive or deleted => delete VCard
     */
    public function syncMember(Member $member): void {
        $this->assertWritePermission();

        $shouldHaveContact = $this->shouldHaveContact($member);
        $hasContact = $member->getContactVcardUri() !== null;

        if ($shouldHaveContact && !$hasContact) {
            $this->createContact($member);
        } elseif ($shouldHaveContact && $hasContact) {
            $this->updateContact($member);
        } elseif (!$shouldHaveContact && $hasContact) {
            $this->deleteContact($member);
        }
    }

    /**
     * Full reconciliation sync for all members.
     *
     * @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) {
                $shouldHaveContact = $this->shouldHaveContact($member);
                $hasContact = $member->getContactVcardUri() !== null;

                if ($shouldHaveContact && !$hasContact) {
                    $this->createContact($member);
                    $stats['created']++;
                } elseif ($shouldHaveContact && $hasContact) {
                    $this->updateContact($member);
                    $stats['updated']++;
                } elseif (!$shouldHaveContact && $hasContact) {
                    $this->deleteContact($member);
                    $stats['deleted']++;
                }
            }

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

        return $stats;
    }

    /**
     * Enqueue a member for contacts sync.
     */
    public function enqueueSync(int $memberId): void {
        $qb = $this->db->getQueryBuilder();
        $qb->insert('mv_sync_queue')
            ->values([
                'entity_type' => $qb->createNamedParameter('contacts'),
                '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 contacts sync', [
                'memberId' => $memberId,
                'exception' => $e,
                'app' => 'mitgliederverwaltung',
            ]);
        }
    }

    /**
     * Process pending contacts 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('contacts')))
            ->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 contact 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;
    }

    // ── CRUD operations ─────────────────────────────────────────────

    private function createContact(Member $member): void {
        $vcardUri = $this->generateVcardUri($member);

        // Try Nextcloud Contacts API first for proper access control integration
        $addressBookKey = $this->findAddressBookKey();
        if ($addressBookKey !== null) {
            $properties = $this->buildContactProperties($member);
            $result = $this->contactsManager->createOrUpdate($properties, $addressBookKey);
            if ($result !== null && isset($result['id'])) {
                $this->updateMemberContactUri($member->getId(), (string)$result['id']);
                $this->logger->debug('Created contact via Nextcloud Contacts API', [
                    'memberId' => $member->getId(),
                    'contactId' => $result['id'],
                    'app' => 'mitgliederverwaltung',
                ]);
                return;
            }
        }

        // Fallback to staging table when Contacts app is not available
        $vcardData = $this->buildVCard($member);
        $this->updateMemberContactUri($member->getId(), $vcardUri);
        $this->storeContactData($vcardUri, $vcardData);

        $this->logger->debug('Created contact VCard (staging table fallback)', [
            'memberId' => $member->getId(),
            'vcardUri' => $vcardUri,
            'app' => 'mitgliederverwaltung',
        ]);
    }

    private function updateContact(Member $member): void {
        $vcardUri = $member->getContactVcardUri();

        // Try Nextcloud Contacts API first for proper access control integration
        $addressBookKey = $this->findAddressBookKey();
        if ($addressBookKey !== null) {
            $properties = $this->buildContactProperties($member);
            // Include the existing contact ID for update
            $properties['id'] = $vcardUri;
            $result = $this->contactsManager->createOrUpdate($properties, $addressBookKey);
            if ($result !== null) {
                $this->logger->debug('Updated contact via Nextcloud Contacts API', [
                    'memberId' => $member->getId(),
                    'app' => 'mitgliederverwaltung',
                ]);
                return;
            }
        }

        // Fallback to staging table
        $vcardData = $this->buildVCard($member);
        $this->storeContactData($vcardUri, $vcardData);

        $this->logger->debug('Updated contact VCard (staging table fallback)', [
            'memberId' => $member->getId(),
            'vcardUri' => $vcardUri,
            'app' => 'mitgliederverwaltung',
        ]);
    }

    public function deleteContact(Member $member): void {
        $this->assertWritePermission();

        $vcardUri = $member->getContactVcardUri();

        if ($vcardUri !== null) {
            $this->removeContactData($vcardUri);
            $this->updateMemberContactUri($member->getId(), null);

            $this->logger->debug('Deleted contact VCard', [
                'memberId' => $member->getId(),
                'vcardUri' => $vcardUri,
                'app' => 'mitgliederverwaltung',
            ]);
        }
    }

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

    /**
     * Build contact properties array for use with OCP\Contacts\IManager::createOrUpdate().
     *
     * @param Member $member The member to build properties for
     * @return array Key-value pairs matching RFC 2426 property names
     */
    private function buildContactProperties(Member $member): array {
        $properties = [
            'FN' => $member->getVorname() . ' ' . $member->getNachname(),
            'N' => [
                $member->getNachname(),
                $member->getVorname(),
                '', '', '',
            ],
            'NOTE' => 'Rolle: ' . $member->getRolle(),
        ];

        if ($member->getGeburtsdatum()) {
            $properties['BDAY'] = $member->getGeburtsdatum();
        }

        // Addresses
        try {
            $addresses = $this->addressMapper->findByMemberId($member->getId());
            $adrList = [];
            foreach ($addresses as $address) {
                $adrList[] = [
                    'type' => $address->getLabel() ?? 'HOME',
                    'value' => [
                        '', '',
                        $address->getStrasse(),
                        $address->getOrt(),
                        '',
                        $address->getPlz(),
                        $address->getLand() ?? 'Deutschland',
                    ],
                ];
            }
            if (!empty($adrList)) {
                $properties['ADR'] = $adrList;
            }
        } catch (\Exception $e) {
            // Skip addresses on error
        }

        // Phones
        try {
            $phones = $this->phoneMapper->findByMemberId($member->getId());
            $telList = [];
            foreach ($phones as $phone) {
                $telList[] = [
                    'type' => $phone->getLabel() ?? 'CELL',
                    'value' => $phone->getNumberE164(),
                ];
            }
            if (!empty($telList)) {
                $properties['TEL'] = $telList;
            }
        } catch (\Exception $e) {
            // Skip phones on error
        }

        // Emails
        try {
            $emails = $this->emailMapper->findByMemberId($member->getId());
            $emailList = [];
            foreach ($emails as $email) {
                $emailList[] = [
                    'type' => $email->getLabel() ?? 'HOME',
                    'value' => $email->getEmail(),
                ];
            }
            if (!empty($emailList)) {
                $properties['EMAIL'] = $emailList;
            }
        } catch (\Exception $e) {
            // Skip emails on error
        }

        return $properties;
    }

    private function shouldHaveContact(Member $member): bool {
        return $member->getStatus() === 'aktiv'
            && $member->getDeletedAt() === null;
    }

    /**
     * Build a VCard for a member with all contact details.
     */
    private function buildVCard(Member $member): string {
        $vcard = new VCard();

        // Name
        $vcard->add('FN', $member->getVorname() . ' ' . $member->getNachname());
        $vcard->add('N', [
            $member->getNachname(),
            $member->getVorname(),
            '', '', ''
        ]);

        // Birthday
        if ($member->getGeburtsdatum()) {
            $vcard->add('BDAY', $member->getGeburtsdatum());
        }

        // Role as note
        $vcard->add('NOTE', 'Rolle: ' . $member->getRolle());

        // Addresses
        try {
            $addresses = $this->addressMapper->findByMemberId($member->getId());
            foreach ($addresses as $address) {
                $adrParts = [
                    '', '', // PO Box, Extended Address
                    $address->getStrasse(),
                    $address->getOrt(),
                    '', // Region
                    $address->getPlz(),
                    $address->getLand() ?? 'Deutschland',
                ];
                $adr = $vcard->add('ADR', $adrParts);
                if ($address->getLabel()) {
                    $adr['TYPE'] = $address->getLabel();
                }
            }
        } catch (\Exception $e) {
            // Skip addresses on error
        }

        // Phones
        try {
            $phones = $this->phoneMapper->findByMemberId($member->getId());
            foreach ($phones as $phone) {
                $tel = $vcard->add('TEL', $phone->getNumberE164());
                if ($phone->getLabel()) {
                    $tel['TYPE'] = $phone->getLabel();
                }
            }
        } catch (\Exception $e) {
            // Skip phones on error
        }

        // Emails
        try {
            $emails = $this->emailMapper->findByMemberId($member->getId());
            foreach ($emails as $email) {
                $emailProp = $vcard->add('EMAIL', $email->getEmail());
                if ($email->getLabel()) {
                    $emailProp['TYPE'] = $email->getLabel();
                }
            }
        } catch (\Exception $e) {
            // Skip emails on error
        }

        return $vcard->serialize();
    }

    private function generateVcardUri(Member $member): string {
        return 'mv-contact-' . $member->getId() . '.vcf';
    }

    private function storeContactData(string $vcardUri, string $vcardData): void {
        $qb = $this->db->getQueryBuilder();
        $qb->select('id')
            ->from('mv_contact_cards')
            ->where($qb->expr()->eq('vcard_uri', $qb->createNamedParameter($vcardUri)));

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

        if ($existing) {
            $updateQb = $this->db->getQueryBuilder();
            $updateQb->update('mv_contact_cards')
                ->set('vcard_data', $updateQb->createNamedParameter($vcardData))
                ->set('updated_at', $updateQb->createNamedParameter(
                    (new \DateTime())->format('Y-m-d H:i:s')
                ))
                ->where($updateQb->expr()->eq('vcard_uri', $updateQb->createNamedParameter($vcardUri)));
            $updateQb->executeStatement();
        } else {
            $insertQb = $this->db->getQueryBuilder();
            $insertQb->insert('mv_contact_cards')
                ->values([
                    'vcard_uri' => $insertQb->createNamedParameter($vcardUri),
                    'vcard_data' => $insertQb->createNamedParameter($vcardData),
                    'addressbook_uri' => $insertQb->createNamedParameter(self::ADDRESSBOOK_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();
        }
    }

    private function removeContactData(string $vcardUri): void {
        $qb = $this->db->getQueryBuilder();
        $qb->delete('mv_contact_cards')
            ->where($qb->expr()->eq('vcard_uri', $qb->createNamedParameter($vcardUri)));
        $qb->executeStatement();
    }

    private function updateMemberContactUri(int $memberId, ?string $uri): void {
        $qb = $this->db->getQueryBuilder();
        $qb->update('mv_members')
            ->set('contact_vcard_uri', $qb->createNamedParameter($uri))
            ->where($qb->expr()->eq('id', $qb->createNamedParameter($memberId, IQueryBuilder::PARAM_INT)));
        $qb->executeStatement();
    }
}
