<?php

namespace App\Services\MicrosoftGraph;

use App\Services\MockMicrosoftGraphService;
use Dcblogdev\MsGraph\MsGraphAdmin;
use Illuminate\Support\Facades\Log;
use Exception;

/**
 * Base client for Microsoft Graph API operations
 * Handles connection, authentication, and basic API operations
 */
class MicrosoftGraphApiClient
{
    protected $msGraphAdmin;
    protected $mockService;
    protected $dryRun = false;

    public function __construct()
    {
        // Initialize services but don't connect during migrations
        $this->msGraphAdmin = new MsGraphAdmin();
        $this->mockService = new MockMicrosoftGraphService();
    }

    /**
     * Check if Microsoft Graph operations are available
     */
    public static function isAvailable(): bool
    {
        try {
            // Check if required table exists
            return app('db')->connection()->getSchemaBuilder()->hasTable('ms_graph_tokens');
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Initialize the service with connection
     */
    public function initialize(): self
    {
        if (self::isAvailable()) {
            $this->ensureConnection();
        }
        return $this;
    }

    /**
     * Set dry run mode
     */
    public function setDryRun(bool $dryRun): self
    {
        $this->dryRun = $dryRun;
        return $this;
    }

    /**
     * Get dry run status
     */
    public function isDryRun(): bool
    {
        return $this->dryRun;
    }

    /**
     * Ensure Microsoft Graph connection is established
     */
    protected function ensureConnection(): void
    {
        if (!$this->dryRun && self::isAvailable() && !$this->msGraphAdmin->isConnected()) {
            $this->msGraphAdmin->connect(false);
        }
    }

    /**
     * Force a fresh connection to Microsoft Graph
     */
    public function forceReconnection(): void
    {
        if (!$this->dryRun) {
            Log::info("🔄 Forcing fresh Microsoft Graph connection...");
            $this->msGraphAdmin->connect(false);
        }
    }

    /**
     * Get the Microsoft Graph service (real or mock)
     */
    public function getGraphService()
    {
        Log::info("🔧 getGraphService() called - dryRun: " . ($this->dryRun ? 'TRUE' : 'FALSE'));

        if ($this->dryRun) {
            Log::info("🧪 Returning MOCK service");
            return $this->mockService;
        }

        Log::info("🔥 Returning REAL Microsoft Graph service");
        // Ensure connection before using real API
        $this->ensureConnection();
        return $this->msGraphAdmin;
    }

    /**
     * Test connection to Microsoft Graph
     */
    public function testConnection(): array
    {
        Log::info("🔗 Testing Microsoft Graph connection...");

        $result = [
            'status' => 'error',
            'message' => 'Unknown error',
            'details' => []
        ];

        try {
            if ($this->dryRun) {
                $result['status'] = 'success';
                $result['message'] = 'Dry run mode - connection not tested';
                $result['details'] = ['mode' => 'dry_run'];
                return $result;
            }

            if (!self::isAvailable()) {
                $result['message'] = 'Microsoft Graph service not available (ms_graph_tokens table missing)';
                return $result;
            }

            // Test basic connection
            $this->ensureConnection();

            if (!$this->msGraphAdmin->isConnected()) {
                $result['message'] = 'Failed to establish connection to Microsoft Graph';
                return $result;
            }

            // Test a simple API call
            $response = $this->msGraphAdmin->get('me');

            if (!$response) {
                $result['message'] = 'Connected but failed to retrieve user information';
                return $result;
            }

            $result['status'] = 'success';
            $result['message'] = 'Successfully connected to Microsoft Graph';
            $result['details'] = [
                'user_id' => $this->getResponseValue($response, 'id') ?? 'unknown',
                'user_name' =>  $this->getResponseValue($response, 'displayName') ?? 'unknown'
            ];

        } catch (Exception $e) {
            $result['message'] = 'Connection test failed: ' . $e->getMessage();
            $result['details'] = ['exception' => get_class($e)];
            Log::error("Microsoft Graph connection test failed", [
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
        }

        return $result;
    }

    /**
     * Get all paginated items from an endpoint with retry logic
     */
    public function getAllPaginatedItems(string $endpoint): array
    {
        $allItems = [];
        $nextLink = null;
        $pageCount = 0;

        // Add $top parameter to get more items per page (max 999)
        $separator = strpos($endpoint, '?') !== false ? '&' : '?';
        $currentEndpoint = $endpoint . $separator . '$top=999';

        Log::info("📄 Getting all paginated items from: {$currentEndpoint}");

        do {
            $pageCount++;
            try {
                Log::info("📄 Fetching page {$pageCount}: " . (strlen($currentEndpoint) > 100 ? substr($currentEndpoint, 0, 100) . '...' : $currentEndpoint));

                $response = null;
                if ($nextLink) {
                    // For next pages, use the full URL from nextLink with retry
                    $response = $this->getWithRetry($nextLink, [
                        'operation' => 'pagination',
                        'page' => $pageCount
                    ]);
                } else {
                    // For first page, use the endpoint with $top parameter with retry
                    $response = $this->getWithRetry($currentEndpoint, [
                        'operation' => 'pagination',
                        'page' => $pageCount
                    ]);
                }

                if (!$response) {
                    Log::warning("📄 No response from API for page {$pageCount}");
                    break;
                }

                // Extract items from response
                $items = $this->getResponseItems($response);
                $itemsCount = count($items);
                $allItems = array_merge($allItems, $items);

                Log::info("📄 Page {$pageCount}: Found {$itemsCount} items. Total so far: " . count($allItems));

                // Check for next page
                $nextLink = $this->getResponseValue($response, '@odata.nextLink');
                if ($nextLink) {
                    Log::info("📄 Found next page link for page " . ($pageCount + 1));
                } else {
                    Log::info("📄 No more pages found. Pagination complete.");
                }

            } catch (Exception $e) {
                Log::error("📄 Error fetching paginated items on page {$pageCount}", [
                    'endpoint' => strlen($currentEndpoint) > 100 ? substr($currentEndpoint, 0, 100) . '...' : $currentEndpoint,
                    'error' => $e->getMessage()
                ]);
                break;
            }
        } while ($nextLink);

        Log::info("📄 Pagination complete. Total items: " . count($allItems) . " across {$pageCount} page(s)");
        return $allItems;
    }

    /**
     * Get a specific value from API response
     */
    public function getResponseValue($response, string $key)
    {
        if (is_array($response)) {
            return $response[$key] ?? null;
        }

        if (is_object($response)) {
            return $response->{$key} ?? null;
        }

        return null;
    }

    /**
     * Check if response has items
     */
    public function hasResponseItems($response): bool
    {
        if (is_array($response)) {
            return isset($response['value']) && is_array($response['value']);
        }

        if (is_object($response)) {
            return isset($response->value) && is_array($response->value);
        }

        return false;
    }

    /**
     * Get items array from API response
     */
    public function getResponseItems($response): array
    {
        if (is_array($response)) {
            return $response['value'] ?? [];
        }

        if (is_object($response)) {
            return $response->value ?? [];
        }

        return [];
    }

    /**
     * Maximum number of retry attempts for throttled requests
     */
    const MAX_RETRY_ATTEMPTS = 3;

    /**
     * Default delay in seconds if Retry-After header is not present
     */
    const DEFAULT_RETRY_DELAY = 1;

    /**
     * Execute a request with automatic retry on throttling
     */
    public function executeWithRetry(callable $request, array $context = []): mixed
    {
        $attempt = 0;
        $lastException = null;

        while ($attempt < self::MAX_RETRY_ATTEMPTS) {
            $attempt++;

            try {
                Log::info("🔄 Executing request (attempt {$attempt})", $context);
                return $request();

            } catch (Exception $e) {
                $lastException = $e;

                if ($this->isThrottlingError($e)) {
                    $retryDelay = $this->getRetryDelay($e);
                    $errorType = $this->getErrorType($e);

                    Log::warning("⏳ {$errorType} (attempt {$attempt}/{" . self::MAX_RETRY_ATTEMPTS . "}). Waiting {$retryDelay} seconds...", [
                        'error' => $e->getMessage(),
                        'retry_delay' => $retryDelay,
                        'context' => $context
                    ]);

                    if ($attempt < self::MAX_RETRY_ATTEMPTS) {
                        // Don't sleep in dry run mode
                        if (!$this->isDryRun()) {
                            sleep($retryDelay);
                        }
                        continue;
                    }
                } else {
                    // Non-throttling error, don't retry
                    throw $e;
                }
            }
        }

        // All retry attempts exhausted
        Log::error("❌ Request failed after " . self::MAX_RETRY_ATTEMPTS . " attempts", [
            'final_error' => $lastException->getMessage(),
            'context' => $context
        ]);

        throw $lastException;
    }

    /**
     * Check if an exception is a retryable error (throttling or service unavailable)
     */
    protected function isThrottlingError(Exception $e): bool
    {
        $errorMessage = $e->getMessage();

        // Check for common throttling indicators
        return str_contains($errorMessage, '429') ||
               str_contains($errorMessage, 'Too Many Requests') ||
               str_contains($errorMessage, 'TooManyRequests') ||
               str_contains($errorMessage, 'Request rate is large') ||
               // Add 503 Service Unavailable for retry
               str_contains($errorMessage, '503') ||
               str_contains($errorMessage, 'Service Unavailable') ||
               str_contains($errorMessage, 'UnknownError');
    }

    /**
     * Get the type of error for logging purposes
     */
    protected function getErrorType(Exception $e): string
    {
        $errorMessage = $e->getMessage();

        if (str_contains($errorMessage, '503') ||
            str_contains($errorMessage, 'Service Unavailable') ||
            str_contains($errorMessage, 'UnknownError')) {
            return 'Service unavailable error';
        }

        if (str_contains($errorMessage, '429') ||
            str_contains($errorMessage, 'Too Many Requests') ||
            str_contains($errorMessage, 'TooManyRequests') ||
            str_contains($errorMessage, 'Request rate is large')) {
            return 'Request throttled';
        }

        return 'Retryable error';
    }

    /**
     * Extract retry delay from exception or use default
     */
    protected function getRetryDelay(Exception $e): int
    {
        $errorMessage = $e->getMessage();

        // Try to extract Retry-After header value from error message
        if (preg_match('/Retry-After:\s*(\d+)/', $errorMessage, $matches)) {
            return (int) $matches[1];
        }

        // Try to extract from different error message formats
        if (preg_match('/retry after (\d+) seconds/i', $errorMessage, $matches)) {
            return (int) $matches[1];
        }

        // For 503 Service Unavailable errors, wait 60 seconds as requested
        if (str_contains($errorMessage, '503') ||
            str_contains($errorMessage, 'Service Unavailable') ||
            str_contains($errorMessage, 'UnknownError')) {
            return 60; // 1 minute delay for service unavailable errors
        }

        // Default exponential backoff based on current attempt for throttling
        return self::DEFAULT_RETRY_DELAY * 2; // 2 seconds default for throttling
    }

    /**
     * Wrapper for GET requests with retry logic
     */
    public function getWithRetry(string $endpoint, array $context = []): mixed
    {
        return $this->executeWithRetry(function() use ($endpoint) {
            return $this->getGraphService()->get($endpoint);
        }, array_merge($context, ['method' => 'GET', 'endpoint' => $endpoint]));
    }

    /**
     * Wrapper for POST requests with retry logic
     */
    public function postWithRetry(string $endpoint, array $data, array $context = []): mixed
    {
        return $this->executeWithRetry(function() use ($endpoint, $data) {
            return $this->getGraphService()->post($endpoint, $data);
        }, array_merge($context, ['method' => 'POST', 'endpoint' => $endpoint]));
    }

    /**
     * Wrapper for DELETE requests with retry logic
     */
    public function deleteWithRetry(string $endpoint, array $context = []): mixed
    {
        return $this->executeWithRetry(function() use ($endpoint) {
            return $this->getGraphService()->delete($endpoint);
        }, array_merge($context, ['method' => 'DELETE', 'endpoint' => $endpoint]));
    }

    /**
     * Wrapper for PATCH requests with retry logic
     */
    public function patchWithRetry(string $endpoint, array $data, array $context = []): mixed
    {
        return $this->executeWithRetry(function() use ($endpoint, $data) {
            return $this->getGraphService()->patch($endpoint, $data);
        }, array_merge($context, ['method' => 'PATCH', 'endpoint' => $endpoint]));
    }

    /**
     * Add a small delay between operations to prevent throttling
     */
    public function preventThrottling(string $operation, int $operationCount): void
    {
        // Add small delays every certain number of operations
        if ($operationCount > 0 && $operationCount % 10 === 0) {
            $delay = 1; // 1 second delay every 10 operations
            MicrosoftGraphUtilities::logOperation('info', "Added {$delay}s delay after {$operationCount} {$operation} operations to prevent throttling");

            // Don't sleep in dry run mode
            if (!$this->isDryRun()) {
                sleep($delay);
            }
        }
    }
}
