<?php

declare(strict_types=1);

namespace App\Application\OAuth;

use App\Framework\Attributes\Route;
use App\Framework\Core\Method;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\JsonResult;
use App\Framework\Http\Redirect;
use App\Framework\Http\StateKey;
use App\Framework\OAuth\OAuthProvider;
use App\Framework\OAuth\Providers\AppleMusicProvider;
use App\Framework\OAuth\Providers\SpotifyProvider;
use App\Framework\OAuth\Providers\TidalProvider;
use App\Framework\OAuth\Storage\OAuthTokenRepository;
use App\Framework\OAuth\Storage\StoredOAuthToken;

/**
 * OAuth Callback Controller
 *
 * Handles OAuth callback redirects from Spotify, Apple Music, and Tidal
 */
final readonly class OAuthCallbackController
{
    public function __construct(
        private OAuthTokenRepository $tokenRepository,
        private SpotifyProvider $spotifyProvider,
        private AppleMusicProvider $appleMusicProvider,
        private TidalProvider $tidalProvider
    ) {
    }

    #[Route(path: '/oauth/spotify/callback', method: Method::GET)]
    public function spotifyCallback(HttpRequest $request): Redirect
    {
        return $this->handleCallback($request, $this->spotifyProvider, 'spotify');
    }

    #[Route(path: '/oauth/apple-music/callback', method: Method::GET)]
    public function appleMusicCallback(HttpRequest $request): Redirect
    {
        return $this->handleCallback($request, $this->appleMusicProvider, 'apple_music');
    }

    #[Route(path: '/oauth/tidal/callback', method: Method::GET)]
    public function tidalCallback(HttpRequest $request): Redirect
    {
        return $this->handleCallback($request, $this->tidalProvider, 'tidal');
    }

    /**
     * Generic callback handler for all OAuth providers
     */
    private function handleCallback(
        HttpRequest $request,
        OAuthProvider $provider,
        string $providerName
    ): Redirect {
        // Get authorization code and state from query params
        $code = $request->query->get('code');
        $state = $request->query->get('state');
        $error = $request->query->get('error');
        $errorDescription = $request->query->get('error_description');

        // Handle OAuth errors
        if ($error !== null) {
            return $this->handleOAuthError($error, $errorDescription);
        }

        // Validate required parameters
        if ($code === null) {
            throw FrameworkException::create(
                ErrorCode::AUTH_INVALID_CREDENTIALS,
                'Missing authorization code'
            )->withData(['provider' => $providerName]);
        }

        // Validate state for CSRF protection
        $sessionState = $request->getSession()->get(StateKey::fromString("oauth_state_{$providerName}"));

        if ($state === null || $sessionState === null || $state !== $sessionState) {
            throw FrameworkException::create(
                ErrorCode::SEC_CSRF_TOKEN_INVALID,
                'Invalid OAuth state - potential CSRF attack'
            )->withData([
                'provider' => $providerName,
                'state_received' => $state !== null,
                'state_in_session' => $sessionState !== null,
            ]);
        }

        // Exchange code for access token
        try {
            $token = $provider->getAccessToken($code, $state);
        } catch (\Exception $e) {
            throw FrameworkException::create(
                ErrorCode::AUTH_TOKEN_EXCHANGE_FAILED,
                "Failed to exchange authorization code for access token"
            )->withData([
                'provider' => $providerName,
                'error' => $e->getMessage(),
            ]);
        }

        // Get user ID from session (should be set before OAuth redirect)
        $userId = $request->getSession()->get(StateKey::fromString('oauth_user_id'));

        if ($userId === null) {
            throw FrameworkException::create(
                ErrorCode::AUTH_SESSION_INVALID,
                'User ID not found in session'
            )->withData(['provider' => $providerName]);
        }

        // Store token in database
        $storedToken = StoredOAuthToken::create(
            userId: $userId,
            provider: $providerName,
            accessToken: $token->accessToken->toString(),
            refreshToken: $token->refreshToken?->toString(),
            tokenType: $token->tokenType,
            scope: $token->scope,
            expiresAt: $token->expiresAt
        );

        $this->tokenRepository->save($storedToken);

        // Clean up session state
        $request->getSession()->forget(StateKey::fromString("oauth_state_{$providerName}"));

        // Get redirect URL from session or use default
        $redirectUrl = $request->getSession()->get(StateKey::fromString('oauth_redirect_url')) ?? '/dashboard';
        $request->getSession()->forget(StateKey::fromString('oauth_redirect_url'));

        // Redirect back with success message
        return Redirect::to($redirectUrl)
            ->withFlashMessage('success', "{$providerName} successfully connected!");
    }

    /**
     * Handle OAuth errors from provider
     */
    private function handleOAuthError(?string $error, ?string $errorDescription): Redirect
    {
        $message = match ($error) {
            'access_denied' => 'You denied access to the application',
            'invalid_request' => 'Invalid OAuth request',
            'unauthorized_client' => 'Application is not authorized',
            'unsupported_response_type' => 'Unsupported OAuth response type',
            'invalid_scope' => 'Invalid OAuth scope requested',
            'server_error' => 'OAuth provider server error',
            'temporarily_unavailable' => 'OAuth provider temporarily unavailable',
            default => $errorDescription ?? 'OAuth authentication failed'
        };

        return Redirect::to('/dashboard')
            ->withFlashMessage('error', $message);
    }

    /**
     * Initiate OAuth flow for a provider
     */
    #[Route(path: '/oauth/{provider}/authorize', method: Method::GET)]
    public function authorize(HttpRequest $request): Redirect
    {
        $providerName = $request->routeParams->get('provider');

        $provider = match ($providerName) {
            'spotify' => $this->spotifyProvider,
            'apple-music', 'apple_music' => $this->appleMusicProvider,
            'tidal' => $this->tidalProvider,
            default => throw FrameworkException::create(
                ErrorCode::HTTP_NOT_FOUND,
                "Unknown OAuth provider: {$providerName}"
            )
        };

        // Normalize provider name
        $normalizedProvider = match ($providerName) {
            'apple-music' => 'apple_music',
            default => $providerName
        };

        // Generate and store state for CSRF protection
        $state = bin2hex(random_bytes(16));
        $request->getSession()->put(StateKey::fromString("oauth_state_{$normalizedProvider}"), $state);

        // Store user ID in session for callback
        $userId = $request->query->get('user_id') ?? $request->getSession()->get(StateKey::fromString('user_id'));

        if ($userId === null) {
            throw FrameworkException::create(
                ErrorCode::AUTH_SESSION_INVALID,
                'User ID required for OAuth flow'
            );
        }

        $request->getSession()->put(StateKey::fromString('oauth_user_id'), $userId);

        // Store redirect URL for after OAuth completes
        $redirectUrl = $request->query->get('redirect_url');
        if ($redirectUrl !== null) {
            $request->getSession()->put(StateKey::fromString('oauth_redirect_url'), $redirectUrl);
        }

        // Get authorization URL from provider
        $authUrl = $provider->getAuthorizationUrl(['state' => $state]);

        return Redirect::to($authUrl);
    }

    /**
     * Disconnect OAuth provider
     */
    #[Route(path: '/oauth/{provider}/disconnect', method: Method::POST)]
    public function disconnect(HttpRequest $request): JsonResult
    {
        $providerName = $request->routeParams->get('provider');
        $userId = $request->getSession()->get(StateKey::fromString('user_id'));

        if ($userId === null) {
            throw FrameworkException::create(
                ErrorCode::AUTH_SESSION_INVALID,
                'User not authenticated'
            );
        }

        // Normalize provider name
        $normalizedProvider = match ($providerName) {
            'apple-music' => 'apple_music',
            default => $providerName
        };

        // Find token
        $storedToken = $this->tokenRepository->findForUser($userId, $normalizedProvider);

        if ($storedToken === null) {
            return new JsonResult([
                'success' => false,
                'message' => 'Provider not connected',
            ], 404);
        }

        // Try to revoke token with provider
        try {
            $provider = match ($normalizedProvider) {
                'spotify' => $this->spotifyProvider,
                'apple_music' => $this->appleMusicProvider,
                'tidal' => $this->tidalProvider,
                default => null
            };

            if ($provider !== null) {
                $token = $storedToken->toOAuthToken();
                $provider->revokeToken($token);
            }
        } catch (\Exception $e) {
            // Log but continue - we'll delete locally anyway
        }

        // Delete token from database
        $this->tokenRepository->delete($storedToken);

        return new JsonResult([
            'success' => true,
            'message' => "{$normalizedProvider} successfully disconnected",
        ]);
    }
}
