<?php

namespace App\Services;

use App\User;
use Exception;
use App\Models\UserType;
use App\Models\ApiCredential;
use Illuminate\Support\Facades\Log;
use App\Repositories\AzureDirectoryRepository;
use App\Services\Integrations\AzureDirectoryIntegration;

class AzureDirectoryService
{
    /**
     * Constructor for AzureDirectoryRepository.
     *
     * @param AzureDirectoryRepository $repository The repository for Azure Directory operations.
     */
    public function __construct(protected AzureDirectoryRepository $repository, protected AzureDirectoryIntegration $integration)
    {
        $this->repository   = $repository;
        $this->integration  = $integration;
    }

    /**
     * Retrieves the credentials for the Azure Active Directory API.
     *
     * This method returns an instance of the ApiCredential model for the
     * 'azure_directory' slug, or null if the credentials do not exist or
     * cannot be loaded.
     *
     * @return ApiCredential|null The Azure Active Directory API credentials.
     */
    public function getCredentials()
    {
        try {
            return ApiCredential::where('slug', 'azure_directory')->first();
        } catch (Exception $e) {
            Log::error('Failed to load Azure credentials', ['error' => $e->getMessage()]);
            return null;
        }
    }

    /**
     * Checks the connection to the Azure Active Directory API.
     *
     * This method calls the checkConnection method of the AzureDirectoryIntegration
     * class and returns the result. If the connection is successful, it returns an
     * array with a key 'success' set to true. If the connection fails, it returns
     * an array with the key 'error' set to the error message.
     *
     * @return array An array with the connection status and error message.
     */
    public function checkConnection()
    {
        $response = $this->integration->checkConnection();

        if ($response === true) {
            return ['success' => true];
        }

        if (isset($response['token_error'])) {
            return ['success' => false, 'error' => $response['token_error']];
        }

        if (isset($response['error'])) {
            return ['success' => false, 'error' => $response['error']['error']['message'] ?? 'Azure API error'];
        }

        return ['success' => false, 'error' => 'Failed to connect to Azure'];
    }

    /**
     * Synchronizes Azure Active Directory data with the local database.
     *
     * This method retrieves user data from Azure Active Directory using pagination
     * and saves it into the local database. It continues to fetch and save user data
     * until there are no more pages of data available.
     *
     * @return int|false The number of users successfully synchronized, or false if an error occurred.
     */

    public function syncActiveDirectoryData()
    {
        $accessToken = $this->integration->getCurrentAccessToken();
        if (empty($accessToken)) {
            return false;
        }

        $count      = 0;
        $nextLink   = null;

        do {
            $response = $this->integration->getUsers($nextLink);

            if ($response === false) {
                return false;
            }

            $count      = $this->saveUsers($response['data'], $count);
            $nextLink   = $response['nextLink'];
        } while ($nextLink != null);

        return $count;
    }

    /**
     * Synchronizes Azure AD users with the local database
     * 
     * @param array $response Array of user data from Azure AD
     * @param int $count Current count of processed users (for batch tracking)
     * @return int Updated count of processed users
     */
    public function saveUsers(array $response, int $count): int
    {
        // Get the UserType ID for 'people' or log error if not found
        $userTypeId = UserType::people()->first()->id ?? null;
        if (!$userTypeId) {
            Log::channel('daily')->error('Missing UserType ID for "people"');
            return $count;
        }

        // Process each user in the response
        foreach ($response as $item) {
            // Parse the raw API item into standardized user data
            $userData = $this->parseUserData($item);

            // Skip if essential fields are missing
            if (empty($userData['email']) || empty($userData['azure_id'])) {
                Log::warning('Skipping user - missing email or Azure ID', ['azure_id' => $userData['azure_id'], 'email' => $userData['email'] ?? 'null']);
                continue;
            }

            try {
                // Find existing user by azure_id or email
                $user = User::where('azure_id', $userData['azure_id'])->orWhere('email', $userData['email'])->first();

                if ($user) {
                    // Update existing user
                    $user->update($userData);
                } else {
                    // Create new user with the user_type_id
                    $userData['user_type_id'] = $userTypeId;
                    $user = User::create($userData);
                }

                $count++; // Increment processed count

                // Rate limiting - pause every 100 records
                if ($count % 100 === 0) {
                    sleep(1);
                }
            } catch (Exception $e) {
                Log::error('Failed to save user', ['azure_id' => $userData['azure_id'], 'email' => $userData['email'] ?? 'null', 'error' => $e->getMessage()]);
            }
        }

        return $count;
    }

    /**
     * Normalizes Azure AD user data to our database format
     * 
     * @param array $item Raw user data from Azure AD
     * @return array Normalized user data for our database
     */
    private function parseUserData(array $item): array
    {
        // Extract email with fallback to userPrincipalName
        $email = $item['mail'] ?? $this->extractUserPrincipalEmail($item);

        // Parse dates only once with null coalescing
        $createdAt      = !empty($item['createdDateTime']) ? general_parse_datetime($item['createdDateTime']) : null;
        $hireDate       = !empty($item['employeeHireDate']) ? general_parse_datetime($item['employeeHireDate']) : null;
        $terminatedDate = !empty($item['employeeLeaveDateTime']) ? general_parse_datetime($item['employeeLeaveDateTime']) : null;

        return [
            'first_name'        => $item['givenName'] ?? $item['displayName'], // Fallback to displayName
            'last_name'         => $item['surname'] ?? '',
            'email'             => $email,
            'city'              => $item['city'] ?? null,
            'employee_id'       => $item['employeeId'] ?? null,
            'azure_id'          => $item['id'] ?? null,
            'status'            => $item['accountEnabled'] ? 1 : 0,
            'country_id'        => $this->repository->getCountryId($item['country'] ?? null),
            'department_id'     => $this->repository->getDepartmentId($item['department'] ?? null),
            'position_id'       => $this->repository->getPositionId($item['jobTitle'] ?? null),
            'employee_type_id'  => $this->repository->getEmployeeTypeId($item['employeeType'] ?? null),
            'created_at'        => $createdAt,
            'hire_date'         => $hireDate,
            'terminated_date'   => $terminatedDate,
        ];
    }

    /**
     * Extracts the user principal email from the given item.
     * 
     * Iterates over the identities in the item to find the identity with the 
     * 'signInType' of 'userPrincipalName' and returns its 'issuerAssignedId' 
     * in lowercase. If no such identity is found, returns null.
     * 
     * @param array $item The user data containing identities.
     * 
     * @return string|null The user principal email or null if not found.
     */

    private function extractUserPrincipalEmail($item)
    {
        foreach ($item['identities'] ?? [] as $identity) {
            if ($identity['signInType'] === 'userPrincipalName') {
                return strtolower($identity['issuerAssignedId']);
            }
        }
        return null;
    }

    /**
     * Check the user has a valid group
     */
    // public function checkUserHasValidGroup($userId)
    // {    
    //     return $this->apiService->checkUserGroups($userId);
    // }

    /**
     * Stores the given access token in the database.
     *
     * @param \League\OAuth2\Client\Token\AccessToken $accessToken The access token to store.
     *
     * @return void
     */
    public function storeTokens($accessToken)
    {
        $credentials = $this->getCredentials();
        $credentials->update([
            'data' => json_encode([
                'accessToken'   => $accessToken->getToken(),
                'refreshToken'  => $accessToken->getRefreshToken(),
                'tokenExpires'  => $accessToken->getExpires(),
            ])
        ]);
    }

    /**
     * Clears the stored access tokens.
     *
     * This method updates the API credentials database entry by setting the 'data' field to an empty string.
     *
     * @return void
     */
    public function clearTokens()
    {
        $credentials = $this->getCredentials();
        $credentials->update(['data' => '']);
    }

    /**
     * Stores the Azure Directory API credentials in the database.
     * 
     * The credentials are stored in the ApiCredential model under the slug 'azure_directory'.
     * 
     * The following data is expected in the $data array:
     * 
     * - tenant: The tenant URL of the Azure Directory.
     * - client_id: The client ID of the Azure Directory application.
     * - client_secret: The client secret of the Azure Directory application.
     * 
     * @param array $data The data to be stored.
     * 
     * @return ApiCredential The created ApiCredential model.
     * 
     * @throws Exception If storing the credentials fails.
     */
    public function storeCredentials(array $data)
    {
        try {
            return ApiCredential::create([
                'api_name'  => 'Azure Directory',
                'slug'      => 'azure_directory',
                'url'       => $data['tenant'],
                'user_name' => $data['client_id'],
                'password'  => $data['client_secret']
            ]);
        } catch (Exception $e) {
            Log::error('Failed to store Azure credentials', ['error' => $e->getMessage()]);
            throw $e;
        }
    }

    /**
     * Retrieve the Azure Directory API credential by its ID.
     *
     * Attempts to find the credential with the given ID in the database.
     * If the credential is not found, logs an error and rethrows the exception.
     *
     * @param int $id The ID of the credential to be retrieved.
     * 
     * @return ApiCredential The found ApiCredential model.
     * 
     * @throws Exception If the credential cannot be found.
     */

    public function findCredential($id)
    {
        try {
            return ApiCredential::findOrFail($id);
        } catch (Exception $e) {
            Log::error('Failed to find Azure credentials', ['id' => $id, 'error' => $e->getMessage()]);
            throw $e;
        }
    }

    /**
     * Updates the Azure Directory API credentials in the database.
     * 
     * Updates the existing credential with the given ID using the provided data.
     * 
     * The following data is expected in the $data array:
     * 
     * - tenant: The tenant URL of the Azure Directory.
     * - client_id: The client ID of the Azure Directory application.
     * - client_secret: The client secret of the Azure Directory application (optional).
     * 
     * @param int $id The ID of the credential to be updated.
     * @param array $data The data to be used for the update.
     * 
     * @return ApiCredential The updated ApiCredential model.
     * 
     * @throws Exception If updating the credential fails.
     */
    public function updateCredentials($id, array $data)
    {
        try {
            $credential = $this->findCredential($id);

            $updateData = [
                'url'       => $data['tenant'],
                'user_name' => $data['client_id']
            ];

            if (!empty($data['client_secret'])) {
                $updateData['password'] = $data['client_secret'];
            }

            $credential->update($updateData);

            return $credential;
        } catch (Exception $e) {
            Log::error('Failed to update Azure credentials', ['id' => $id, 'error' => $e->getMessage()]);
            throw $e;
        }
    }
}
