<?php

namespace App\Http\Controllers\Settings;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Artisan;
use App\Services\AzureDirectoryService;
use App\Http\Requests\Settings\AzureDirectoryRequest;
use App\Services\Integrations\AzureDirectoryIntegration;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;

class AzureDirectoryCredentialController extends Controller
{

    protected $service;
    protected $integration;

    /**
     * Create a new controller instance.
     *
     * @param AzureDirectoryService $service
     *
     * @return void
     */
    public function __construct(AzureDirectoryService $service, AzureDirectoryIntegration $integration)
    {
        $this->service      = $service;
        $this->integration  = $integration;
    }

    /**
     * Index page for Azure Directory settings.
     *
     * If the user has already created the Azure Directory API credential, show the user the settings page.
     * Otherwise, redirect the user to the create page
     *
     * @return object View is being returned.
     */
    public function index()
    {
        $credentials = $this->service->getCredentials();

        return $credentials
            ? view('settings.azure-directory.index', ['credentials' => $credentials, 'status' => 0])
            : redirect()->route('azure-directory.create');
    }

    /**
     * Page for creating the Azure Directory API credential.
     *
     * If the API credential already exists, redirect the user to the edit page.
     * Otherwise, show the user the create page.
     *
     * @return object View is being returned.
     */
    public function create()
    {
        $credential = $this->service->getCredentials();
        return $credential
            ? redirect()->route('azure-directory.edit', $credential->id)
            : view('settings.azure-directory.create');
    }

    /**
     * Stores the Azure Directory API credentials in the database.
     *
     * The credentials are validated by the AzureDirectoryRequest form request.
     * If the validation fails, the user is redirected back with the validation errors.
     * If the validation passes, the credentials are stored in the database.
     * The user is redirected back to the index page with a success or error message.
     *
     * @param AzureDirectoryRequest $request The form request containing the Azure Directory API credentials.
     *
     * @return \Illuminate\Http\RedirectResponse Redirects to the index page with a success or error message.
     */
    public function store(AzureDirectoryRequest $request)
    {
        try {
            $this->service->storeCredentials($request->all());
            return redirect()->route('azure-directory.index')->with('message', __('message.created'));
        } catch (Exception $e) {
            return redirect()->route('azure-directory.index')->with('error', 'Something went wrong. Try again later');
        }
    }

    /**
     * Show the edit page for the specified Azure Directory API credential.
     *
     * The page will show the existing credential data for the user to edit.
     * If the credential does not exist, redirect the user to the index page
     * with an error message.
     *
     * @param int $id The ID of the credential to be edited.
     *
     * @return object View is being returned.
     */
    public function edit($id)
    {
        try {
            $credential = $this->service->findCredential($id);
            return view('settings.azure-directory.edit', compact('credential'));
        } catch (Exception $e) {
            return redirect()->route('azure-directory.index')->with('error', 'Something went wrong. Try again later');
        }
    }

    /**
     * Updates the Azure Directory API credentials.
     *
     * Updates the existing API credentials based on the data provided in the request.
     * Redirects to the index page with a success or error message depending on the result.
     *
     * @param AzureDirectoryRequest $request The request containing the credential data.
     * @param int $id The ID of the credential to be updated.
     *
     * @return \Illuminate\Http\RedirectResponse Redirects to the index page with a success or error message.
     */
    public function update(AzureDirectoryRequest $request, $id)
    {
        try {
            $this->service->updateCredentials($id, $request->all());
            return redirect()->route('azure-directory.index')->with('message', __('message.updated'));
        } catch (Exception $e) {
            return redirect()->route('azure-directory.index')->with('error', 'Something went wrong. Try again later');
        }
    }

    /**
     * Redirects the user to the Azure AD authorization page to initiate the connection.
     *
     * Redirects to the Azure AD authorization page using the URL provided by the
     * service. If an error occurs, logs the error and redirects to the index page
     * with an error message.
     *
     * @return \Illuminate\Http\RedirectResponse Redirects to the Azure AD authorization page.
     */
    public function connect()
    {
        try {
            $authUrl = $this->integration->getAuthUrl();
            return redirect()->away($authUrl);
        } catch (Exception $e) {
            Log::error('Failed to initiate Azure AD connection', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
            return redirect()->route('azure-directory.index')->with('error', 'Failed to initiate connection. Please try again.');
        }
    }

    /**
     * Callback function for Azure Directory OAuth authorization flow.
     *
     * Handles the callback from Azure Directory after the user has authorized the app.
     * Validates the state parameter, checks for authorization code, exchanges the code
     * for tokens, and stores the tokens in the database.
     *
     * @param Request $request The HTTP request containing the authorization code
     *                         and state parameter.
     *
     * @return \Illuminate\Http\RedirectResponse Redirects to the index page with
     *                                          a success or error message.
     */
    public function callback(Request $request)
    {
        try {
            // Validate state parameter
            $expectedState = session('oauthState');
            $providedState = $request->query('state');
            $request->session()->forget('oauthState');

            if (!isset($expectedState)) {
                Log::error('Azure OAuth - Missing state in session');
                return redirect('/azure-directory')->with('error', 'Session expired. Please initiate the connection again.');
            }

            if (!isset($providedState)) {
                Log::error('Azure OAuth - No state parameter received');
                return redirect('/azure-directory')->with('error', 'Invalid authentication response')->with('errorDetail', 'Missing security parameter.');
            }

            if ($expectedState != $providedState) {
                Log::error('Azure OAuth - State mismatch');
                return redirect('/azure-directory')->with('error', 'Security validation failed');
            }

            // Check for authorization code
            $authCode = $request->query('code');
            if (!$authCode) {
                $errorDesc = $request->query('error_description', 'No authorization code received');
                Log::error('Azure OAuth - Authorization failed', ['error' => $errorDesc]);
                return redirect('/azure-directory')->with('error', 'Authorization failed')->with('errorDetail', $errorDesc);
            }

            // Exchange auth code for tokens
            try {
                $accessToken = $this->integration->getAccessToken($authCode);

                if (!$accessToken) {
                    Log::error('Azure OAuth - Empty access token received');
                    return redirect('/azure-directory')->with('error', 'Authentication failed')->with('errorDetail', 'No valid access token received.');
                }

                $this->service->storeTokens($accessToken);
                return redirect('/azure-directory')->with('message', 'Successfully connected to Azure Directory');
            } catch (IdentityProviderException $e) {
                Log::error('Azure OAuth - Token exchange error', ['error' => $e->getMessage()]);
                return redirect('/azure-directory')->with('error', 'Failed to complete authentication')->with('errorDetail', $e->getMessage());
            }
        } catch (Exception $e) {
            Log::error('Azure OAuth - Unexpected error', ['error' => $e->getMessage()]);
            return redirect('/azure-directory')->with('error', 'Unexpected system error');
        }
    }

    /**
     * Tests the connection to Azure AD.
     *
     * This method redirects to the Azure AD authorization page and attempts to
     * validate the connection. If the connection is successful, a message is
     * displayed. If the connection fails, an error message is displayed.
     *
     * @return \Illuminate\Http\RedirectResponse Redirects to the Azure AD authorization page.
     */
    public function test()
    {
        try {
            $response = $this->service->checkConnection();

            if (isset($response['error'])) {
                $errorMessage = $response['error']['error']['message'] ?? 'Unknown API error';
                Log::error('Azure AD connection test failed', ['error' => $errorMessage]);
                return redirect('/azure-directory')->with('error', "Connection Failed")->with('errorDetail', $errorMessage);
            }

            if (isset($response['token_error'])) {
                Log::error('Azure AD connection test failed', ['error' => $response['token_error']]);
                return redirect('/azure-directory')->with('error', "Connection Failed")->with('errorDetail', $response['token_error']);
            }

            return redirect('/azure-directory')->with('message', 'Connection Successful');
        } catch (Exception $e) {
            Log::error('Azure AD connection test error', ['error' => $e->getMessage()]);
            return redirect('/azure-directory')->with('error', 'Connection Failed')->with('errorDetail', $e->getMessage());
        }
    }

    /**
     * Signs out of Azure AD by clearing the stored access tokens.
     *
     * Redirects to the Azure Directory index page after signing out.
     *
     * @return \Illuminate\Http\RedirectResponse Redirects to the Azure Directory index page.
     */
    public function signout()
    {
        $this->service->clearTokens();
        return redirect('/azure-directory');
    }

    /**
     * Triggers a manual synchronization of the Azure Directory data.
     *
     * This function is used to manually trigger the Azure Directory data
     * synchronization process. It uses the `AzureDirectory:azuredirectory` Artisan
     * command to do the synchronization. The function sets unlimited execution time
     * and memory limit before calling the command. The function catches any
     * exceptions that may occur during the synchronization process and logs the
     * error. The function returns a JSON response with the status (success, warning,
     * or error) and a message describing the result of the synchronization process.
     *
     * @return \Illuminate\Http\JsonResponse The JSON response containing the status
     *                                       and message of the synchronization process.
     */
    public function manualSync()
    {
        setUnlimitedExecutionTimeAndMemoryLimit();

        try {

            $exitCode   = Artisan::call('AzureDirectory:azuredirectory');
            $message    = Artisan::output();

            return response()->json([
                'status'    => $exitCode === 0 ? 'success' : 'warning',
                'message'   => trim($message)
            ]);
        } catch (Exception $e) {
            Log::error('Azure Directory sync error', ['error' => $e->getMessage()]);
            return response()->json(['status'    => 'error', 'message'   => 'Sync failed: ' . $e->getMessage()]);
        }
    }
}
