<?php

namespace App\Services\HubSpot;

use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectInputForCreate;
use HubSpot\Client\Crm\Contacts\Model\Filter;
use HubSpot\Client\Crm\Contacts\Model\FilterGroup;
use HubSpot\Client\Crm\Contacts\Model\PublicObjectSearchRequest;
use Illuminate\Support\Facades\Log;

/**
 * HubSpot Contact Service
 *
 * Handles all contact-related operations in HubSpot including
 * creating contacts and managing list memberships.
 */
class HubSpotContactService
{
    /**
     * @var HubSpotApiClient
     */
    protected $apiClient;

    /**
     * @var bool Whether we've checked for the sync property
     */
    protected $syncPropertyChecked = false;

    /**
     * @var bool Whether the sync property actually exists in HubSpot
     */
    protected $syncPropertyExists = false;

    /**
     * Create a new HubSpotContactService instance.
     *
     * @param HubSpotApiClient $apiClient
     */
    public function __construct(HubSpotApiClient $apiClient)
    {
        $this->apiClient = $apiClient;
    }

    /**
     * Get the sync property name from config.
     *
     * @return string
     */
    protected function getSyncPropertyName(): string
    {
        return config('hubspot.contact_property.name', 'server_sync_source');
    }

    /**
     * Get the sync property value from config.
     *
     * @return string
     */
    protected function getSyncPropertyValue(): string
    {
        return config('hubspot.contact_property.value', 'efet_intranet');
    }

    /**
     * Ensure the sync property exists in HubSpot.
     * Creates it if it doesn't exist.
     *
     * @return bool True if property exists or was created, false if creation failed (permission issue)
     */
    public function ensureSyncPropertyExists(): bool
    {
        if ($this->syncPropertyChecked) {
            return $this->syncPropertyExists;
        }

        if ($this->apiClient->isDryRun()) {
            Log::info("[DRY-RUN] Would ensure sync property exists: " . $this->getSyncPropertyName());
            $this->syncPropertyChecked = true;
            $this->syncPropertyExists = true;
            return true;
        }

        $propertyName = $this->getSyncPropertyName();

        try {
            return $this->apiClient->executeWithRetry(function () use ($propertyName) {
                $client = $this->apiClient->getClient();

                // Check if property exists
                try {
                    $client->crm()->properties()->coreApi()->getByName('contacts', $propertyName);
                    Log::info("Sync property '{$propertyName}' already exists in HubSpot");
                    $this->syncPropertyChecked = true;
                    $this->syncPropertyExists = true;
                    return true;
                } catch (\Exception $e) {
                    // Property doesn't exist, try to create it
                    if (strpos($e->getMessage(), '404') === false) {
                        throw $e;
                    }
                }

                // Create the property
                $propertyCreate = new \HubSpot\Client\Crm\Properties\Model\PropertyCreate([
                    'name' => $propertyName,
                    'label' => config('hubspot.contact_property.label', 'Server Sync Source'),
                    'type' => 'string',
                    'field_type' => 'text',
                    'group_name' => config('hubspot.contact_property.group', 'contactinformation'),
                    'description' => 'Indicates this contact was created/managed by server synchronization',
                ]);

                $client->crm()->properties()->coreApi()->create('contacts', $propertyCreate);

                Log::info("Created sync property '{$propertyName}' in HubSpot");
                $this->syncPropertyChecked = true;
                $this->syncPropertyExists = true;

                $this->apiClient->applyRateLimit();

                return true;
            }, 'ensureSyncPropertyExists');
        } catch (\Exception $e) {
            // If we can't create the property (permission issue), log warning and continue
            if (strpos($e->getMessage(), '403') !== false) {
                Log::warning("Cannot create sync property '{$propertyName}' - missing crm.schemas.contacts.write scope. Continuing without sync property tracking.");
                // Mark as checked to prevent repeated attempts
                $this->syncPropertyChecked = true;
                $this->syncPropertyExists = false;
                return false;
            }
            throw $e;
        }
    }

    /**
     * Check if the sync property is available.
     *
     * @return bool
     */
    public function isSyncPropertyAvailable(): bool
    {
        if (!$this->syncPropertyChecked) {
            $this->ensureSyncPropertyExists();
        }
        return $this->syncPropertyExists;
    }

    /**
     * Check if a contact has the sync property set.
     *
     * @param string $contactId
     * @return bool Returns true if property exists and matches, or true if property not available (allow removal)
     */
    public function isServerSyncContact(string $contactId): bool
    {
        // If sync property is not available, consider all contacts as "server synced" for removal purposes
        if (!$this->isSyncPropertyAvailable()) {
            Log::debug("Sync property not available, assuming contact {$contactId} is server-synced");
            return true;
        }

        return $this->apiClient->executeWithRetry(function () use ($contactId) {
            $client = $this->apiClient->getClient();
            $propertyName = $this->getSyncPropertyName();
            $expectedValue = $this->getSyncPropertyValue();

            try {
                $contact = $client->crm()->contacts()->basicApi()->getById(
                    $contactId,
                    [$propertyName]
                );

                $properties = $contact->getProperties();
                return isset($properties[$propertyName]) && $properties[$propertyName] === $expectedValue;
            } catch (\Exception $e) {
                Log::warning("Could not check sync property for contact {$contactId}: " . $e->getMessage());
                return false;
            }
        }, 'isServerSyncContact: ' . $contactId);
    }

    /**
     * Mark a contact as server-synced by setting the sync property.
     *
     * @param string $contactId
     * @return bool
     */
    public function markAsServerSyncContact(string $contactId): bool
    {
        // Skip if sync property is not available
        if (!$this->isSyncPropertyAvailable()) {
            return true;
        }

        if ($this->apiClient->isDryRun()) {
            Log::info("[DRY-RUN] Would mark contact {$contactId} as server-synced");
            return true;
        }

        return $this->apiClient->executeWithRetry(function () use ($contactId) {
            $client = $this->apiClient->getClient();
            $propertyName = $this->getSyncPropertyName();
            $propertyValue = $this->getSyncPropertyValue();

            $updateInput = new \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectInput([
                'properties' => [
                    $propertyName => $propertyValue,
                ],
            ]);

            $client->crm()->contacts()->basicApi()->update($contactId, $updateInput);

            Log::debug("Marked contact {$contactId} as server-synced");

            $this->apiClient->applyRateLimit();

            return true;
        }, 'markAsServerSyncContact: ' . $contactId);
    }

    /**
     * Mark multiple contacts as server-synced in batch.
     *
     * @param array $contactIds
     * @return int Number of contacts marked
     */
    public function markContactsAsServerSynced(array $contactIds): int
    {
        if (empty($contactIds)) {
            return 0;
        }

        // Skip if sync property is not available
        if (!$this->isSyncPropertyAvailable()) {
            Log::info("Skipping marking contacts as server-synced - property not available");
            return 0;
        }

        if ($this->apiClient->isDryRun()) {
            Log::info("[DRY-RUN] Would mark " . count($contactIds) . " contacts as server-synced");
            return count($contactIds);
        }

        $marked = 0;
        $propertyName = $this->getSyncPropertyName();
        $propertyValue = $this->getSyncPropertyValue();
        $batchSize = $this->apiClient->getBatchSize();
        $batches = array_chunk($contactIds, $batchSize);

        foreach ($batches as $batch) {
            try {
                $this->apiClient->executeWithRetry(function () use ($batch, $propertyName, $propertyValue) {
                    $client = $this->apiClient->getClient();

                    $inputs = array_map(function ($id) use ($propertyName, $propertyValue) {
                        return new \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectBatchInput([
                            'id' => (string) $id,
                            'properties' => [
                                $propertyName => $propertyValue,
                            ],
                        ]);
                    }, $batch);

                    $batchInput = new \HubSpot\Client\Crm\Contacts\Model\BatchInputSimplePublicObjectBatchInput([
                        'inputs' => $inputs,
                    ]);

                    $client->crm()->contacts()->batchApi()->update($batchInput);
                }, 'markContactsAsServerSynced');

                $marked += count($batch);
                Log::info("Marked " . count($batch) . " contacts as server-synced");
            } catch (\Exception $e) {
                Log::error("Failed to mark batch of contacts as server-synced: " . $e->getMessage());
            }

            $this->apiClient->applyRateLimit();
        }

        return $marked;
    }

    /**
     * Get a contact by email.
     *
     * @param string $email
     * @return array|null
     */
    public function getContactByEmail(string $email): ?array
    {
        $email = strtolower(trim($email));

        return $this->apiClient->executeWithRetry(function () use ($email) {
            $client = $this->apiClient->getClient();

            $filter = new Filter([
                'property_name' => 'email',
                'operator' => 'EQ',
                'value' => $email,
            ]);

            $filterGroup = new FilterGroup(['filters' => [$filter]]);

            $searchRequest = new PublicObjectSearchRequest([
                'filter_groups' => [$filterGroup],
                'properties' => ['email', 'firstname', 'lastname'],
                'limit' => 1,
            ]);

            $response = $client->crm()->contacts()->searchApi()->doSearch($searchRequest);
            $results = $response->getResults() ?? [];

            if (empty($results)) {
                return null;
            }

            $contact = $results[0];
            $properties = $contact->getProperties();

            return [
                'contactId' => $contact->getId(),
                'email' => $properties['email'] ?? $email,
                'firstName' => $properties['firstname'] ?? null,
                'lastName' => $properties['lastname'] ?? null,
            ];
        }, 'getContactByEmail: ' . $email);
    }

    /**
     * Create a new contact in HubSpot.
     * Automatically sets the server sync property if available.
     *
     * @param string $email
     * @param string|null $firstName
     * @param string|null $lastName
     * @param array $additionalProperties
     * @return array|null The created contact info or null if dry-run
     */
    public function createContact(string $email, ?string $firstName = null, ?string $lastName = null, array $additionalProperties = []): ?array
    {
        $email = strtolower(trim($email));

        if ($this->apiClient->isDryRun()) {
            Log::info("[DRY-RUN] Would create HubSpot contact: {$email}");
            return [
                'contactId' => 'dry-run-' . md5($email),
                'email' => $email,
                'firstName' => $firstName,
                'lastName' => $lastName,
                'dryRun' => true,
            ];
        }

        return $this->apiClient->executeWithRetry(function () use ($email, $firstName, $lastName, $additionalProperties) {
            $client = $this->apiClient->getClient();

            // Start with base properties
            $properties = array_merge([
                'email' => $email,
            ], $additionalProperties);

            // Include the server sync property if available
            if ($this->isSyncPropertyAvailable()) {
                $properties[$this->getSyncPropertyName()] = $this->getSyncPropertyValue();
            }

            if ($firstName !== null) {
                $properties['firstname'] = $firstName;
            }

            if ($lastName !== null) {
                $properties['lastname'] = $lastName;
            }

            $contactInput = new SimplePublicObjectInputForCreate([
                'properties' => $properties,
            ]);

            $contact = $client->crm()->contacts()->basicApi()->create($contactInput);

            $syncMsg = $this->isSyncPropertyAvailable() ? ' with sync property' : '';
            Log::info("Created HubSpot contact: {$email} (ID: {$contact->getId()}){$syncMsg}");

            $this->apiClient->applyRateLimit();

            return [
                'contactId' => $contact->getId(),
                'email' => $email,
                'firstName' => $firstName,
                'lastName' => $lastName,
                'serverSync' => $this->isSyncPropertyAvailable(),
            ];
        }, 'createContact: ' . $email);
    }

    /**
     * Ensure a contact exists, create if not.
     *
     * @param string $email
     * @param string|null $firstName
     * @param string|null $lastName
     * @param array $additionalProperties
     * @return array The contact info (existing or newly created)
     */
    public function ensureContactExists(string $email, ?string $firstName = null, ?string $lastName = null, array $additionalProperties = []): array
    {
        $existing = $this->getContactByEmail($email);

        if ($existing !== null) {
            return $existing;
        }

        return $this->createContact($email, $firstName, $lastName, $additionalProperties);
    }

    /**
     * Get all contact IDs in a list.
     *
     * @param string $listId
     * @return array Array of contact IDs (as strings)
     */
    public function getContactIdsInList(string $listId): array
    {
        return $this->apiClient->executeWithRetry(function () use ($listId) {
            $client = $this->apiClient->getClient();
            $contactIds = [];
            $after = null;

            do {
                $response = $client->crm()->lists()->membershipsApi()->getPage(
                    $listId,
                    $after,
                    null, // $before — not used for forward pagination
                    100   // limit
                );

                $results = $response->getResults() ?? [];
                foreach ($results as $membership) {
                    // The API returns membership objects with recordId property
                    if (is_object($membership) && method_exists($membership, 'getRecordId')) {
                        $contactIds[] = (string) $membership->getRecordId();
                    } elseif (is_array($membership) && isset($membership['recordId'])) {
                        $contactIds[] = (string) $membership['recordId'];
                    } elseif (is_string($membership) || is_numeric($membership)) {
                        // Fallback for simple ID values
                        $contactIds[] = (string) $membership;
                    }
                }

                $paging = $response->getPaging();
                $after = $paging?->getNext()?->getAfter();

                if ($after) {
                    $this->apiClient->applyRateLimit();
                }
            } while ($after !== null);

            Log::debug("Retrieved " . count($contactIds) . " contact IDs from list {$listId}");

            return $contactIds;
        }, 'getContactIdsInList: ' . $listId);
    }

    /**
     * Get all contact emails in a list.
     *
     * @param string $listId
     * @return array Array of emails (lowercase)
     */
    public function getContactEmailsInList(string $listId): array
    {
        $contactIds = $this->getContactIdsInList($listId);

        if (empty($contactIds)) {
            return [];
        }

        $emails = [];
        $batchSize = $this->apiClient->getBatchSize();
        $batches = array_chunk($contactIds, $batchSize);

        foreach ($batches as $batch) {
            $batchEmails = $this->getEmailsForContactIds($batch);
            $emails = array_merge($emails, $batchEmails);
            $this->apiClient->applyRateLimit();
        }

        return array_map('strtolower', $emails);
    }

    /**
     * Get emails for a batch of contact IDs.
     *
     * @param array $contactIds
     * @return array
     */
    protected function getEmailsForContactIds(array $contactIds): array
    {
        // Filter out empty/invalid IDs and ensure they are strings
        $validIds = array_filter(array_map(function ($id) {
            $stringId = (string) $id;
            return !empty($stringId) && $stringId !== '0' ? $stringId : null;
        }, $contactIds));

        if (empty($validIds)) {
            return [];
        }

        return $this->apiClient->executeWithRetry(function () use ($validIds) {
            $client = $this->apiClient->getClient();
            $emails = [];

            // Use batch read API - ensure IDs are strings
            $inputs = array_map(function ($id) {
                return new \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId(['id' => (string) $id]);
            }, $validIds);

            $batchReadInput = new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId([
                'inputs' => array_values($inputs), // Re-index array
                'properties' => ['email'],
            ]);

            $response = $client->crm()->contacts()->batchApi()->read($batchReadInput);
            $results = $response->getResults() ?? [];

            foreach ($results as $contact) {
                $props = $contact->getProperties();
                if (!empty($props['email'])) {
                    $emails[] = $props['email'];
                }
            }

            return $emails;
        }, 'getEmailsForContactIds');
    }

    /**
     * Add contacts to a list by email.
     * Automatically marks existing contacts with the server sync property.
     *
     * @param string $listId
     * @param array $emails
     * @return array ['added' => count, 'failed' => [...emails]]
     */
    public function addContactsToList(string $listId, array $emails): array
    {
        if (empty($emails)) {
            return ['added' => 0, 'failed' => []];
        }

        // Ensure sync property exists
        $this->ensureSyncPropertyExists();

        $added = 0;
        $failed = [];
        $contactIds = [];
        $existingContactIds = []; // Track existing contacts that need to be marked

        // First, ensure all contacts exist and get their IDs
        foreach ($emails as $email) {
            $email = strtolower(trim($email));
            try {
                // Check if contact already exists
                $existingContact = $this->getContactByEmail($email);

                if ($existingContact) {
                    // Contact exists, track it for marking
                    $contactIds[] = $existingContact['contactId'];
                    $existingContactIds[] = $existingContact['contactId'];
                } else {
                    // Create new contact (will automatically have sync property)
                    $newContact = $this->createContact($email);
                    if ($newContact && isset($newContact['contactId'])) {
                        $contactIds[] = $newContact['contactId'];
                    }
                }
            } catch (\Exception $e) {
                Log::error("Failed to ensure contact exists: {$email} - " . $e->getMessage());
                $failed[] = $email;
            }
        }

        if (empty($contactIds)) {
            return ['added' => 0, 'failed' => $failed];
        }

        if ($this->apiClient->isDryRun()) {
            Log::info("[DRY-RUN] Would add " . count($contactIds) . " contacts to list {$listId}");
            return ['added' => count($contactIds), 'failed' => $failed];
        }

        // Mark existing contacts with sync property (in batches)
        if (!empty($existingContactIds)) {
            $this->markContactsAsServerSynced($existingContactIds);
        }

        // Add contacts to list in batches
        $batchSize = $this->apiClient->getBatchSize();
        $batches = array_chunk($contactIds, $batchSize);

        foreach ($batches as $batch) {
            try {
                $this->addContactIdsToList($listId, $batch);
                $added += count($batch);
            } catch (\Exception $e) {
                Log::error("Failed to add batch to list {$listId}: " . $e->getMessage());
                // We don't know which specific contacts failed in batch
            }
            $this->apiClient->applyRateLimit();
        }

        Log::info("Added {$added} contacts to HubSpot list {$listId}");

        return ['added' => $added, 'failed' => $failed];
    }

    /**
     * Add contact IDs to a list.
     *
     * @param string $listId
     * @param array $contactIds
     * @return void
     */
    protected function addContactIdsToList(string $listId, array $contactIds): void
    {
        $this->apiClient->executeWithRetry(function () use ($listId, $contactIds) {
            $client = $this->apiClient->getClient();

            $client->crm()->lists()->membershipsApi()->add(
                $listId,
                $contactIds
            );
        }, 'addContactIdsToList: ' . $listId);
    }

    /**
     * Remove contacts from a list by email.
     *
     * @param string $listId
     * @param array $emails
     * @return array ['removed' => count, 'failed' => [...emails]]
     */
    public function removeContactsFromList(string $listId, array $emails): array
    {
        if (empty($emails)) {
            return ['removed' => 0, 'failed' => []];
        }

        $removed = 0;
        $failed = [];
        $contactIds = [];

        // Get contact IDs for emails
        foreach ($emails as $email) {
            $email = strtolower(trim($email));
            try {
                $contact = $this->getContactByEmail($email);
                if ($contact && isset($contact['contactId'])) {
                    $contactIds[] = $contact['contactId'];
                }
            } catch (\Exception $e) {
                Log::error("Failed to get contact for removal: {$email} - " . $e->getMessage());
                $failed[] = $email;
            }
        }

        if (empty($contactIds)) {
            return ['removed' => 0, 'failed' => $failed];
        }

        if ($this->apiClient->isDryRun()) {
            Log::info("[DRY-RUN] Would remove " . count($contactIds) . " contacts from list {$listId}");
            return ['removed' => count($contactIds), 'failed' => $failed];
        }

        // Remove contacts from list in batches
        $batchSize = $this->apiClient->getBatchSize();
        $batches = array_chunk($contactIds, $batchSize);

        foreach ($batches as $batch) {
            try {
                $this->removeContactIdsFromList($listId, $batch);
                $removed += count($batch);
            } catch (\Exception $e) {
                Log::error("Failed to remove batch from list {$listId}: " . $e->getMessage());
            }
            $this->apiClient->applyRateLimit();
        }

        Log::info("Removed {$removed} contacts from HubSpot list {$listId}");

        return ['removed' => $removed, 'failed' => $failed];
    }

    /**
     * Remove contact IDs from a list.
     *
     * @param string $listId
     * @param array $contactIds
     * @return void
     */
    protected function removeContactIdsFromList(string $listId, array $contactIds): void
    {
        $this->apiClient->executeWithRetry(function () use ($listId, $contactIds) {
            $client = $this->apiClient->getClient();

            $client->crm()->lists()->membershipsApi()->remove(
                $listId,
                $contactIds
            );
        }, 'removeContactIdsFromList: ' . $listId);
    }

    /**
     * Add a single contact to a list by email.
     *
     * @param string $listId
     * @param string $email
     * @return bool
     */
    public function addContactToList(string $listId, string $email): bool
    {
        $result = $this->addContactsToList($listId, [$email]);
        return $result['added'] > 0;
    }

    /**
     * Remove a single contact from a list by email.
     *
     * @param string $listId
     * @param string $email
     * @return bool
     */
    public function removeContactFromList(string $listId, string $email): bool
    {
        $result = $this->removeContactsFromList($listId, [$email]);
        return $result['removed'] > 0;
    }

    /**
     * Sync contacts in a list: add missing, remove extra (only server-synced contacts).
     *
     * @param string $listId
     * @param array $expectedEmails Emails that should be in the list
     * @return array ['added' => [...], 'removed' => [...], 'kept' => [...], 'skipped' => [...]]
     */
    public function syncContactsInList(string $listId, array $expectedEmails): array
    {
        // Ensure sync property exists before syncing
        $this->ensureSyncPropertyExists();

        // Normalize emails
        $expectedEmails = array_map(function ($email) {
            return strtolower(trim($email));
        }, $expectedEmails);
        $expectedEmails = array_unique($expectedEmails);

        // Get current emails in the list
        $currentEmails = $this->getContactEmailsInList($listId);

        // Calculate differences
        $toAdd = array_diff($expectedEmails, $currentEmails);
        $toRemove = array_diff($currentEmails, $expectedEmails);
        $kept = array_intersect($currentEmails, $expectedEmails);

        $result = [
            'added' => [],
            'removed' => [],
            'kept' => array_values($kept),
            'skipped' => [],
            'errors' => [],
        ];

        // Add new contacts
        if (!empty($toAdd)) {
            $addResult = $this->addContactsToList($listId, array_values($toAdd));
            $result['added'] = array_values($toAdd);
            if (!empty($addResult['failed'])) {
                $result['errors'] = array_merge($result['errors'], array_map(function ($email) {
                    return "Failed to add: {$email}";
                }, $addResult['failed']));
            }
        }

        // Remove old contacts - but only if they are server-synced
        if (!empty($toRemove)) {
            $serverSyncedToRemove = [];
            $skippedManual = [];

            // Check which contacts are server-synced
            foreach ($toRemove as $email) {
                $contact = $this->getContactByEmail($email);
                if ($contact && isset($contact['contactId'])) {
                    if ($this->isServerSyncContact($contact['contactId'])) {
                        $serverSyncedToRemove[] = $email;
                    } else {
                        $skippedManual[] = $email;
                        Log::info("Skipping removal of non-server-synced contact: {$email}");
                    }
                }
            }

            // Only remove server-synced contacts
            if (!empty($serverSyncedToRemove)) {
                $removeResult = $this->removeContactsFromList($listId, $serverSyncedToRemove);
                $result['removed'] = $serverSyncedToRemove;
                if (!empty($removeResult['failed'])) {
                    $result['errors'] = array_merge($result['errors'], array_map(function ($email) {
                        return "Failed to remove: {$email}";
                    }, $removeResult['failed']));
                }
            }

            // Record skipped contacts
            $result['skipped'] = $skippedManual;
        }

        return $result;
    }
}
