<?php

namespace FSPoster\App\SocialNetworks\YoutubeShorts\Api;

use FSPoster\GuzzleHttp\Client;

class Api
{
    public AuthData $authData;
    public ?string  $proxy = null;

    public string $authException = \Exception::class;
    public string $postException = \Exception::class;

    private $client;

    const OAUTH_AUTH_URL  = 'https://accounts.google.com/o/oauth2/v2/auth';
    const OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
    const API_BASE_URL    = 'https://www.googleapis.com/youtube/v3';
    const UPLOAD_BASE_URL = 'https://www.googleapis.com/upload/youtube/v3';
    const SCOPES          = 'https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube.readonly https://www.googleapis.com/auth/youtube.force-ssl';
    const CHUNK_SIZE      = 5242880; // 5 MB (multiple of 256 KB)

    public function setProxy( ?string $proxy ): self
    {
        $this->proxy = $proxy;
        $this->client = null;

        return $this;
    }

    public function setAuthData( AuthData $authData ): self
    {
        $this->authData = $authData;

        return $this;
    }

    public function setAuthException( string $exceptionClass ): self
    {
        $this->authException = $exceptionClass;

        return $this;
    }

    public function setPostException( string $exceptionClass ): self
    {
        $this->postException = $exceptionClass;

        return $this;
    }

    public function getClient()
    {
        if ( is_null( $this->client ) )
        {
            $this->client = new Client( [
                'verify'      => true,
                'proxy'       => $this->proxy,
                'http_errors' => false,
            ] );
        }

        return $this->client;
    }

    /**
     * Build Google OAuth 2.0 authorization URL.
     */
    public static function getAuthURL(string $clientId, string $state, string $callbackUrl): string
    {
        $params = [
            'client_id'     => $clientId,
            'redirect_uri'  => $callbackUrl,
            'response_type' => 'code',
            'scope'         => self::SCOPES,
            'state'         => $state,
            'access_type'   => 'offline',
            'prompt'        => 'consent',
        ];

        return self::OAUTH_AUTH_URL . '?' . http_build_query( $params );
    }

    /**
     * Exchange authorization code for access and refresh tokens.
     */
    public function fetchAccessToken( $code, $callbackUrl ): self
    {
        $postData = [
            'code'          => $code,
            'client_id'     => $this->authData->appClientId,
            'client_secret' => $this->authData->appClientSecret,
            'redirect_uri'  => $callbackUrl,
            'grant_type'    => 'authorization_code',
        ];

        $response = $this->getClient()->post( self::OAUTH_TOKEN_URL, [
            'form_params' => $postData,
        ] );

        $params = json_decode( $response->getBody()->getContents(), true );

        if ( isset( $params['error'] ) )
            throw new $this->authException( $params['error_description'] ?? $params['error'] );

        $this->authData->accessToken = $params['access_token'];
        $this->authData->accessTokenExpiresOn = time() + (int)$params['expires_in'];
        $this->authData->refreshToken = $params['refresh_token'] ?? '';

        return $this;
    }

    /**
     * Refresh the access token using the stored refresh token.
     */
    public function refreshAccessToken(): void
    {
        $postData = [
            'client_id'     => $this->authData->appClientId,
            'client_secret' => $this->authData->appClientSecret,
            'refresh_token' => $this->authData->refreshToken,
            'grant_type'    => 'refresh_token',
        ];

        $response = $this->getClient()->post( self::OAUTH_TOKEN_URL, [
            'form_params' => $postData,
        ] );

        $params = json_decode( $response->getBody()->getContents(), true );

        if ( isset( $params['error'] ) )
            throw new $this->authException( $params['error_description'] ?? $params['error'] );

        $this->authData->accessToken = $params['access_token'];
        $this->authData->accessTokenExpiresOn = time() + (int)$params['expires_in'];
    }

    /**
     * Get the authenticated user's YouTube channel info.
     */
    public function getMyInfo(): array
    {
        $response = $this->apiRequest( self::API_BASE_URL . '/channels', 'GET', [
            'part' => 'snippet',
            'mine' => 'true',
        ] );

        if ( empty( $response['items'] ) )
            throw new $this->authException( fsp__( 'No YouTube channel found for this account.' ) );

        return $response['items'][0];
    }

    /**
     * Get video statistics (likes, comments, views).
     */
    public function getStats( string $videoId ): array
    {
        $response = $this->apiRequest( self::API_BASE_URL . '/videos', 'GET', [
            'part' => 'statistics',
            'id'   => $videoId,
        ] );

        $stats = $response['items'][0]['statistics'] ?? [];

        return [
            [
                'label' => fsp__( 'Likes' ),
                'value' => (int) ( $stats['likeCount'] ?? 0 ),
            ],
            [
                'label' => fsp__( 'Comments' ),
                'value' => (int) ( $stats['commentCount'] ?? 0 ),
            ],
            [
                'label' => fsp__( 'Views' ),
                'value' => (int) ( $stats['viewCount'] ?? 0 ),
            ],
        ];
    }

    /**
     * Upload a video as a YouTube Short via the resumable upload protocol.
     */
    public function sendPost( PostingData $postingData ): string
    {
        $videoFile = $postingData->uploadMedia[0]['path'] ?? null;
        $mimeType  = $postingData->uploadMedia[0]['mime_type'] ?? 'video/mp4';

        if ( empty( $videoFile ) || ! file_exists( $videoFile ) )
            throw new $this->postException( fsp__( 'Video file not found.' ) );

        $fileSize  = filesize( $videoFile );
        $uploadUrl = $this->initResumableUpload( $postingData, $mimeType, $fileSize );
        $videoId   = $this->uploadVideoFile( $uploadUrl, $videoFile, $mimeType, $fileSize );

        $this->postFirstComment( $postingData, $videoId );

        return $videoId;
    }

    /**
     * Post a first comment on the uploaded video if configured.
     */
    private function postFirstComment( PostingData $postingData, string $videoId ): void
    {
        if ( empty( $postingData->firstComment ) || $postingData->privacyLevel === 'private' ) {
            return;
        }

        $commentData = [
            'snippet' => [
                'videoId'      => $videoId,
                'topLevelComment' => [
                    'snippet' => [
                        'textOriginal' => $postingData->firstComment,
                    ],
                ],
            ],
        ];

        $response = $this->getClient()->post(
            self::API_BASE_URL . '/commentThreads?part=snippet',
            [
                'headers' => [
                    'Authorization' => 'Bearer ' . $this->authData->accessToken,
                    'Content-Type'  => 'application/json',
                ],
                'body' => json_encode( $commentData ),
            ]
        );

        $statusCode = $response->getStatusCode();

        if ( $statusCode !== 200 && $statusCode !== 201 )
        {
            $body = json_decode( $response->getBody()->getContents(), true );
            $errorMessage = $body['error']['message'] ?? fsp__( 'Failed to post first comment.' );
            throw new $this->postException( $errorMessage );
        }
    }

    /**
     * Initiate a resumable upload session.
     * Returns the upload URL from the Location header.
     *
     * @see https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol
     * @see https://gist.github.com/dgp/1b24bf2961521bd75d6c
     */
    private function initResumableUpload( PostingData $postingData, string $mimeType, int $fileSize ): string
    {
        $metadata = [
            'snippet' => [
                'title'       => $postingData->title,
                'description' => $postingData->description,
                'categoryId'  => '22',
            ],
            'status' => [
                'privacyStatus'           => $postingData->privacyLevel,
                'selfDeclaredMadeForKids' => false,
            ],
        ];

        $response = $this->getClient()->post(
            self::UPLOAD_BASE_URL . '/videos?uploadType=resumable&part=snippet,status',
            [
                'headers' => [
                    'Authorization'           => 'Bearer ' . $this->authData->accessToken,
                    'Content-Type'            => 'application/json; charset=UTF-8',
                    'X-Upload-Content-Type'   => $mimeType,
                    'X-Upload-Content-Length'  => $fileSize,
                ],
                'body' => json_encode( $metadata ),
            ]
        );

        $statusCode = $response->getStatusCode();
        $uploadUrl  = $response->getHeaderLine( 'Location' );

        if ( $statusCode !== 200 || empty( $uploadUrl ) )
        {
            $body = json_decode( $response->getBody()->getContents(), true );
            $errorMessage = $body['error']['message'] ?? fsp__( 'Failed to initiate video upload.' );
            throw new $this->postException( $errorMessage );
        }

        return $uploadUrl;
    }

    /**
     * Upload the video file in chunks using the resumable upload protocol.
     * Returns the YouTube video ID.
     */
    private function uploadVideoFile( string $uploadUrl, string $filePath, string $mimeType, int $fileSize ): string
    {
        $handle = fopen( $filePath, 'rb' );

        if ( $handle === false )
            throw new $this->postException( fsp__( 'Unable to open video file.' ) );

        try
        {
            $offset = 0;

            while ( $offset < $fileSize )
            {
                $chunkSize = min( self::CHUNK_SIZE, $fileSize - $offset );
                $chunkData = fread( $handle, $chunkSize );

                if ( $chunkData === false )
                    throw new $this->postException( fsp__( 'Failed to read video file during upload.' ) );

                $actualLength = strlen( $chunkData );

                if ( $actualLength === 0 )
                    throw new $this->postException( fsp__( 'Unexpected end of video file during upload.' ) );

                $endByte = $offset + $actualLength - 1;

                $headers = [
                    'Authorization'  => 'Bearer ' . $this->authData->accessToken,
                    'Content-Type'   => $mimeType,
                    'Content-Length'  => $actualLength,
                    'Content-Range'  => sprintf( 'bytes %d-%d/%d', $offset, $endByte, $fileSize ),
                ];

                $response = $this->getClient()->put( $uploadUrl, [
                    'headers' => $headers,
                    'body'    => $chunkData,
                ] );

                $statusCode = $response->getStatusCode();

                if ( $statusCode === 200 || $statusCode === 201 )
                {
                    $body = json_decode( $response->getBody()->getContents(), true );
                    return $body['id'] ?? '';
                }

                if ( $statusCode === 308 )
                {
                    $range = $response->getHeaderLine( 'Range' );

                    if ( ! empty( $range ) && preg_match( '/bytes=0-(\d+)/', $range, $matches ) )
                        $offset = (int)$matches[1] + 1;
                    else
                        $offset += $actualLength;

                    continue;
                }

                $body = json_decode( $response->getBody()->getContents(), true );
                $errorMessage = $body['error']['message'] ?? fsp__( 'Video upload failed.' );
                throw new $this->postException( $errorMessage );
            }
        }
        finally
        {
            fclose( $handle );
        }

        throw new $this->postException( fsp__( 'Video upload completed but no video ID was returned.' ) );
    }

    /**
     * Generic YouTube Data API request with Bearer token auth.
     */
    private function apiRequest( string $url, string $method, $data = [] ): array
    {
        $headers = [
            'Authorization' => 'Bearer ' . $this->authData->accessToken,
        ];

        $options = [ 'headers' => $headers ];

        if ( $method === 'GET' )
        {
            $url .= '?' . http_build_query( $data );
        }
        elseif ( ! empty( $data ) )
        {
            if ( is_array( $data ) )
                $options['form_params'] = $data;
            else
                $options['body'] = $data;
        }

        $response = $this->getClient()->request( $method, $url, $options );

        $body = json_decode( $response->getBody()->getContents(), true );

        if ( ! is_array( $body ) )
            throw new $this->postException( fsp__( 'Something went wrong!' ) );

        if ( isset( $body['error'] ) )
        {
            $errorMessage = $body['error']['message'] ?? fsp__( 'API request failed.' );
            throw new $this->authException( $errorMessage );
        }

        return $body;
    }
}
