<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Models\User;
use App\Services\Auth\PostLoginRedirectResolver;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class EntraController
{
    public function redirect(Request $request): RedirectResponse
    {
        $clientId = (string) config('services.microsoft.client_id');
        $clientSecret = (string) config('services.microsoft.client_secret');
        $redirectUri = (string) config('services.microsoft.redirect');
        $authorityTenant = (string) config('services.microsoft.tenant', 'organizations');

        if ($clientId === '' || $clientSecret === '' || $redirectUri === '') {
            return $this->failRedirect($request, reasonCode: 'oidc_provider_unavailable');
        }

        $state = (string) Str::uuid();
        $request->session()->put('entra_state', $state);

        $scopes = implode(' ', ['openid', 'profile', 'email']);

        $url = sprintf(
            'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize?%s',
            $authorityTenant,
            http_build_query([
                'client_id' => $clientId,
                'response_type' => 'code',
                'redirect_uri' => $redirectUri,
                'response_mode' => 'query',
                'scope' => $scopes,
                'state' => $state,
            ])
        );

        return redirect()->away($url);
    }

    public function callback(Request $request): RedirectResponse
    {
        $expectedState = $request->session()->pull('entra_state');

        if (! is_string($expectedState) || $expectedState === '') {
            return $this->failRedirect($request, reasonCode: 'oidc_invalid_state');
        }

        if ($expectedState !== $request->string('state')->toString()) {
            return $this->failRedirect($request, reasonCode: 'oidc_invalid_state');
        }

        if ($request->string('error')->toString() !== '') {
            return $this->failRedirect($request, reasonCode: 'oidc_user_denied');
        }

        $code = $request->string('code')->toString();

        if ($code === '') {
            return $this->failRedirect($request, reasonCode: 'oidc_provider_unavailable');
        }

        $clientId = (string) config('services.microsoft.client_id');
        $clientSecret = (string) config('services.microsoft.client_secret');
        $redirectUri = (string) config('services.microsoft.redirect');
        $authorityTenant = (string) config('services.microsoft.tenant', 'organizations');

        if ($clientId === '' || $clientSecret === '' || $redirectUri === '') {
            return $this->failRedirect($request, reasonCode: 'oidc_provider_unavailable');
        }

        $response = Http::asForm()->post(
            sprintf('https://login.microsoftonline.com/%s/oauth2/v2.0/token', $authorityTenant),
            [
                'client_id' => $clientId,
                'client_secret' => $clientSecret,
                'code' => $code,
                'grant_type' => 'authorization_code',
                'redirect_uri' => $redirectUri,
            ]
        );

        if ($response->failed()) {
            return $this->failRedirect($request, reasonCode: 'oidc_provider_unavailable');
        }

        $payload = $response->json() ?: [];

        $idToken = $payload['id_token'] ?? null;

        if (! is_string($idToken) || $idToken === '') {
            return $this->failRedirect($request, reasonCode: 'oidc_missing_claims');
        }

        $claims = $this->decodeJwtClaims($idToken);

        $entraTenantId = is_string($claims['tid'] ?? null) ? (string) $claims['tid'] : '';
        $entraObjectId = is_string($claims['oid'] ?? null) ? (string) $claims['oid'] : '';

        if ($entraTenantId === '' || $entraObjectId === '') {
            return $this->failRedirect($request, reasonCode: 'oidc_missing_claims');
        }

        $email = $this->resolveEmailFromClaims($claims, $entraTenantId, $entraObjectId);
        $name = $this->resolveNameFromClaims($claims, $email);

        try {
            $existingUser = User::withTrashed()
                ->where('entra_tenant_id', $entraTenantId)
                ->where('entra_object_id', $entraObjectId)
                ->first();

            if ($existingUser?->trashed()) {
                return $this->failRedirect(
                    $request,
                    reasonCode: 'user_disabled',
                    entraTenantId: $entraTenantId,
                    entraObjectId: $entraObjectId,
                    userId: (int) $existingUser->getKey(),
                );
            }

            $isNewUser = $existingUser === null;
            $user = $existingUser ?? new User;
            $user->fill([
                'entra_tenant_id' => $entraTenantId,
                'entra_object_id' => $entraObjectId,
                'email' => $email,
                'name' => $name,
            ]);

            if ($isNewUser) {
                $user->password = Str::password(64);
            }

            $user->save();
        } catch (\Throwable $exception) {
            return $this->failRedirect(
                $request,
                reasonCode: 'oidc_user_upsert_failed',
                entraTenantId: $entraTenantId,
                entraObjectId: $entraObjectId,
            );
        }

        Auth::login($user);
        $request->session()->regenerate();

        Log::info('auth.entra.login', $this->logContext($request, success: true, entraTenantId: $entraTenantId, entraObjectId: $entraObjectId, userId: (int) $user->getKey()));

        $redirectTo = app(PostLoginRedirectResolver::class)->resolve($user);

        return redirect()->to($redirectTo);
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeJwtClaims(string $jwt): array
    {
        $parts = explode('.', $jwt);

        if (count($parts) < 2) {
            return [];
        }

        $payload = $this->base64UrlDecode($parts[1]);

        if ($payload === null) {
            return [];
        }

        $decoded = json_decode($payload, true);

        return is_array($decoded) ? $decoded : [];
    }

    private function base64UrlDecode(string $value): ?string
    {
        $value = str_replace(['-', '_'], ['+', '/'], $value);
        $padding = strlen($value) % 4;

        if ($padding > 0) {
            $value .= str_repeat('=', 4 - $padding);
        }

        $decoded = base64_decode($value, true);

        return $decoded === false ? null : $decoded;
    }

    /**
     * @param  array<string, mixed>  $claims
     */
    private function resolveEmailFromClaims(array $claims, string $entraTenantId, string $entraObjectId): string
    {
        $candidate = null;

        foreach (['preferred_username', 'email', 'upn'] as $key) {
            $value = $claims[$key] ?? null;

            if (is_string($value) && $value !== '') {
                $candidate = $value;
                break;
            }
        }

        if (! is_string($candidate) || $candidate === '') {
            $candidate = sprintf('%s@%s.entra.invalid', $entraObjectId, $entraTenantId);
        }

        $candidate = strtolower(trim($candidate));

        return Str::limit($candidate, 255, '');
    }

    /**
     * @param  array<string, mixed>  $claims
     */
    private function resolveNameFromClaims(array $claims, string $email): string
    {
        $candidate = $claims['name'] ?? null;

        if (is_string($candidate) && $candidate !== '') {
            return Str::limit(trim($candidate), 255, '');
        }

        $given = $claims['given_name'] ?? null;
        $family = $claims['family_name'] ?? null;

        if (is_string($given) && is_string($family)) {
            $full = trim($given.' '.$family);

            if ($full !== '') {
                return Str::limit($full, 255, '');
            }
        }

        return Str::limit($email, 255, '');
    }

    private function failRedirect(
        Request $request,
        string $reasonCode,
        ?string $entraTenantId = null,
        ?string $entraObjectId = null,
        ?int $userId = null,
    ): RedirectResponse {
        Log::warning('auth.entra.login', $this->logContext(
            $request,
            success: false,
            reasonCode: $reasonCode,
            entraTenantId: $entraTenantId,
            entraObjectId: $entraObjectId,
            userId: $userId,
        ));

        return redirect()
            ->to('/admin/login')
            ->with('error', 'Authentication failed. Please try again.');
    }

    /**
     * @return array{success:bool,reason_code?:string,user_id?:int,entra_tenant_id?:string,entra_object_id_hash?:string,correlation_id:string,timestamp:string}
     */
    private function logContext(
        Request $request,
        bool $success,
        ?string $reasonCode = null,
        ?string $entraTenantId = null,
        ?string $entraObjectId = null,
        ?int $userId = null,
    ): array {
        $correlationId = $request->header('X-Request-Id')
            ?: ($request->hasSession() ? $request->session()->getId() : null)
            ?: (string) Str::uuid();

        $context = [
            'success' => $success,
            'correlation_id' => (string) $correlationId,
            'timestamp' => now()->toISOString(),
        ];

        if ($reasonCode !== null) {
            $context['reason_code'] = $reasonCode;
        }

        if ($userId !== null) {
            $context['user_id'] = $userId;
        }

        if ($entraTenantId !== null) {
            $context['entra_tenant_id'] = $entraTenantId;
        }

        if ($entraObjectId !== null) {
            $context['entra_object_id_hash'] = hash('sha256', $entraObjectId);
        }

        return $context;
    }
}
