<?php
namespace SmartSyncS3\Ajax;
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
use Aws\S3\S3Client;
use Exception;
use SmartSyncS3\Constants\SMARL_Constant;
use SmartSyncS3\Helpers\SMARL_Helper;
use SmartSyncS3\Models\SMARL_CloudConfig;
use SmartSyncS3\Models\SMARL_S3SyncSettings;
use function as_schedule_single_action;

/**
 * Class AjaxHandler
 * Handles all the AJAX actions related to SmartSyncS3 plugin functionalities.
 *
 * This class manages synchronization between the local WordPress file system and AWS S3,
 * toggles CDN settings, clears CloudFront cache, and provides analytics and sync status.
 *
 * @package SmartSyncS3
 * @author Infinitie Technologies
 *
 * Credits:
 * - Developed by Infinitie Technologies
 * - Maintained by the Infintitie Development Team
 * - Inspired by best practices for WordPress plugin development
 *
 */


class SMARL_AjaxHandler
{
    /**
     * @var SMARL_CloudConfig|null $appConfig The configuration object for AWS S3 and CloudFront.
     */
    private ?SMARL_CloudConfig $appConfig;

    /**
     * @var SMARL_S3SyncSettings
     */
    private SMARL_S3SyncSettings $s3SyncSettings;

    /**
     * Constructor to initialize the AJAX handler with a configuration.
     *
     * @param SMARL_CloudConfig|null $configuration The AWS configuration object.
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function __construct(?SMARL_CloudConfig $configuration)
    {

        if($configuration){
            $this->appConfig = $configuration;
        }

        $this->s3SyncSettings = new SMARL_S3SyncSettings();
        $this->s3SyncSettings->load();
    }


    /**
     * Registering AJAX and scheduled actions
     *
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function initializeActions(): void
    {
        add_action('wp_ajax_clear_cloudfront_cache', [$this, 'clearCloudFrontCache']);
        add_action('wp_ajax_toggle_cdn', [$this, 'toggleCdn']);
        add_action('wp_ajax_toggle_media_auto_delete', [$this, 'toggleMediaAutoDelete']);
        add_action('wp_ajax_sync_local_to_aws', [$this, 'syncLocalToAws']);
        add_action('wp_ajax_stop_all_processes', [$this, 'stopAllProcesses']);
        add_action('process_s3_sync_batch', [$this, 'processS3Batch'], 10, 4);
        add_action('wp_ajax_sync_aws_to_local', [$this, 'syncAwsToLocal']);
        add_action('wp_ajax_set_sync_state', [$this, 'setSyncState']);
        add_action('wp_ajax_get_sync_state', [$this, 'getSyncState']);
        add_action('wp_ajax_check_sync_status', [$this, 'checkSyncStatus']);
        add_action('wp_ajax_fetch_aws_analytics', [$this, 'fetchAwsAnalytics']);
    }



    /**
     * Fetch AWS S3 analytics such as total files, storage used, and cost.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function fetchAwsAnalytics(): void
    {
        check_ajax_referer('smarl_nonce', 'nonce');
        $s3_pricing = SMARL_Helper::getRegionPrices();
        $config = $this->appConfig;
        if (!$config) {
            wp_send_json_error('AWS configuration not found.');
        }

        try {
            $s3 = $config->getS3Client();

            $total_size = 0;
            $total_files = 0;

            $args = ['Bucket' => $config->bucket];

            do {
                $objects = $s3->listObjectsV2($args);

                foreach ($objects['Contents'] as $object) {
                    $total_size += $object['Size'];
                    $total_files++;
                }

                $args['ContinuationToken'] = $objects['NextContinuationToken'];
            } while ($objects['IsTruncated']);


            if($this->appConfig->serviceProvider === "aws"){
                $price_in_dollars = ($total_size / (1024 * 1024 * 1024)) * $s3_pricing[$config->region];
            }else{
                $price_in_dollars = SMARL_Helper::calculateStorageDigitalOceanPrice(totalBytes: $total_size);
            }

            wp_send_json_success([
                'total_files' => $total_files,
                'total_storage' => SMARL_Helper::formatBytes(bytes:$total_size),
                'price_in_dollars' => round($price_in_dollars, 4), // Round to 2 decimal places
                "total_size" => $total_size
            ]);
        } catch (Exception $e) {
            wp_send_json_error('Error fetching analytics.');
        }

    }

    /**
     * Check the current synchronization status.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function checkSyncStatus(): void
    {
        check_ajax_referer('smarl_nonce', 'nonce');

        wp_send_json_success([
            'total_files' => $this->s3SyncSettings->s3SyncTotalFiles,
            'processed_files' => $this->s3SyncSettings->s3SyncProcessedFiles,
            'status' => $this->s3SyncSettings->s3SyncStatus,
            'direction' => $this->s3SyncSettings->s3SyncDirection,
            'errors' => $this->s3SyncSettings->s3SyncErrors,
            'successes' => $this->s3SyncSettings->s3SyncSuccesses,
            "type" => $this->s3SyncSettings->syncType
        ]);

    }

    /**
     * Retrieve the current sync state.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function getSyncState(): void{
        check_ajax_referer('smarl_nonce', 'nonce');
        $state = $this->s3SyncSettings->syncState;
        wp_send_json_success($state);
    }

    /**
     * Update the sync state.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function setSyncState(): void
    {
        check_ajax_referer('smarl_nonce', 'nonce');
        $state = isset($_POST['state']) ? sanitize_text_field(wp_unslash($_POST['state'])) : '';
        $this->s3SyncSettings->syncState = $state;
        $this->s3SyncSettings->save();
        wp_send_json_success();

    }

    /**
     * Synchronize files from AWS S3 to the local WordPress file system.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function syncAwsToLocal() : void
    {
        check_ajax_referer('smarl_nonce', 'nonce');

        $config = $this->appConfig;
        if (!$config) {

            $this->noConfigurationFound();
        }

        $type = isset($_POST['type']) ? sanitize_text_field(wp_unslash($_POST['type'])) : '';
        $allowedExtensions = SMARL_Helper::getExtension($type);


        try {
            $s3 = $config->getS3Client();

            $all_files = [];
            $args = ['Bucket' => $config->bucket];

            do {
                $objects = $s3->listObjectsV2($args);
                foreach ($objects['Contents'] as $object) {
                    $extention = (pathinfo($object['Key'], PATHINFO_EXTENSION));
                    if(in_array(needle: $extention, haystack: $allowedExtensions)){
                        $all_files[] = [
                            'key' =>  $object['Key'],
                            'size' => $object['Size']
                        ];
                    }
                }
                $args['ContinuationToken'] = $objects['NextContinuationToken'];
            } while ($objects['IsTruncated']);

            // Initialize sync state
            $this->s3SyncSettings->s3SyncSuccesses = [];
            $this->s3SyncSettings->s3SyncErrors = [];
            $this->s3SyncSettings->syncState = 'syncing';
            $this->s3SyncSettings->s3SyncStatus = "processing";
            $this->s3SyncSettings->s3SyncProcessedFiles = 0;
            $this->s3SyncSettings->s3SyncDirection = 'download';
            $this->s3SyncSettings->s3SyncTotalFiles = count($all_files);
            $this->s3SyncSettings->s3SyncAllFiles = $all_files;
            $this->s3SyncSettings->save();

            // Schedule first batch
            as_schedule_single_action(time(), 'process_s3_sync_batch', [
                'batch_number' => 1,
                "settings" => [],
                'files' => array_slice($all_files, 0, 10),
                'direction' => 'download'
            ]);


            wp_send_json_success([
                'message' => esc_html__('Sync started', 'smartsync-lite-media-offloader-and-assets-cdn'),
                'total_files' => count($all_files)
            ]);

        } catch (Exception $e) {
            wp_send_json_error($e->getMessage());
        }
    }

    public function processS3Batch(int $batch_number, array $settings, array $batch_files, string $direction): void
    {

        $batch_size = SMARL_Constant::$processBatchSize;
        if($this->appConfig->serviceProvider == "google"){
            $batch_size = 2;
        }




        $offset = ($batch_number - 1) * $batch_size;


        try {

            if ($this->s3SyncSettings->s3SyncStatus === 'idle') {
                return;
            }

            $s3 = $this->appConfig->getS3Client();

            $uploads_path = wp_upload_dir();
            $uploads_dir = $uploads_path['basedir'];

            $batch_files = SMARL_Helper::getFileSyncQueueData(limit: $batch_size, offset: $offset);


            if($direction === "upload"){
                if($this->appConfig->serviceProvider === "aws") {
                    $this->uploadFileBatch($batch_files, $s3);
                }
                if($this->appConfig->serviceProvider === "cloudflare") {
                    $this->uploadFileBatch($batch_files, $s3);
                }
                if($this->appConfig->serviceProvider === "digitalocean") {
                    $this->uploadFileBatch($batch_files, $s3);
                }



                foreach ($batch_files as $file) {
                    $this->s3SyncSettings->s3SyncProcessedFiles++;
                }
            }else{
                foreach ($batch_files as $file) {

                    $this->processFile($file, $direction, $uploads_dir, $s3);
                    $this->s3SyncSettings->s3SyncProcessedFiles++;
                }
            }

            $this->updateSyncSettingsAfterBatch($batch_files);

            $this->scheduleNextBatch($batch_number, $batch_size, $direction, $s3);

        } catch (Exception $e) {

            $this->s3SyncSettings->s3SyncErrors = array_merge(
                $this->s3SyncSettings->s3SyncErrors,
                ['Fatal error: ' . $e->getMessage()]
            );
            $type = $this->s3SyncSettings->syncType;

            if($type === "media"){
                $this->appConfig->canEnableMediaCDN = false;
                $this->appConfig->useCdnForMedia = false;
            }else {
                $this->appConfig->canEnableAssetCDN = false;
                $this->appConfig->useCdnForAssets = false;
            }
            $this->s3SyncSettings->syncState = 'default';
            $this->s3SyncSettings->s3SyncStatus = 'failed';

            $this->s3SyncSettings->save();
        }
    }

    private function processFile(array $file, string $direction, string $uploads_dir,S3Client $s3): void
    {
        try {
            if ($direction == "upload" && in_array($file['relative_path'], $this->s3SyncSettings->s3SyncSuccesses)){
                return;
            }
            elseif ($direction == "download" && in_array($file['key'], $this->s3SyncSettings->s3SyncSuccesses)){
                return;
            }


            if ($direction === 'upload') {
                $this->uploadFile($file, $s3);
            } else {

                $this->downloadFile($file, $uploads_dir, $s3);
            }
        } catch (Exception $e) {
            $this->s3SyncSettings->s3SyncErrors[] = sprintf(
                'Failed to %s file: %s',
                $direction === 'upload' ? 'upload' : 'download',
                $e->getMessage()
            );
        }
    }

    private function uploadFile(array $file, $s3): void
    {

        $prepare_object = [
            'Bucket' => $this->appConfig->bucket,
            'Key' => $file['relative_path'],
            'SourceFile' => $file['path'],
        ];

        if ($this->appConfig->serviceProvider !== "aws") {
            $prepare_object["ACL"] = 'public-read';
        }

        $s3->putObject($prepare_object);
        $this->s3SyncSettings->s3SyncSuccesses[] = $file['relative_path'];
    }

    private function uploadFileBatch(array $files, S3Client $s3): void
    {
        $promises = [];

        foreach ($files as $file) {

            $prepare_object = [
                'Bucket' => $this->appConfig->bucket,
                'Key' => $file['relative_path'],
                'SourceFile' => $file['path'],
            ];

            if ($this->appConfig->serviceProvider !== "aws") {
                $prepare_object["ACL"] = 'public-read';
            }

            // Create an async promise instead of uploading immediately
            $promises[$file['relative_path']] = $s3->putObjectAsync($prepare_object);
        }

        // Wait for all promises to complete
        $results = \GuzzleHttp\Promise\Utils::unwrap($promises);

        // Record successful uploads
        foreach ($results as $path => $result) {
            $this->s3SyncSettings->s3SyncSuccesses[] = $path;
        }
    }



    private function downloadFile(array $file, string $uploads_dir, S3Client $s3): void
    {
        $file_path = "$uploads_dir/{$file['key']}";
        wp_mkdir_p(dirname($file_path));
        $s3->getObject([
            'Bucket' => $this->appConfig->bucket,
            'Key' => $file['key'],
            'SaveAs' => $file_path,
        ]);

        $this->s3SyncSettings->s3SyncSuccesses[] = $file['key'];
    }

    private function updateSyncSettingsAfterBatch(array $files): void
    {
        $this->s3SyncSettings->save();
    }

    private function scheduleNextBatch(int $batch_number, int $batch_size, string $direction, $s3): void
    {
        $all_files = $this->s3SyncSettings->s3SyncAllFiles;

        $next_batch = array_slice($all_files, $batch_number * $batch_size, $batch_size);

        if (!empty($next_batch)) {
            as_schedule_single_action(time() + 2, 'process_s3_sync_batch', [
                'batch_number' => $batch_number + 1,
                'settings' => [],
                'files' => $next_batch,
                'direction' => $direction,
            ]);
        } else {
            $this->s3SyncSettings->syncState = "default";
            $this->s3SyncSettings->s3SyncStatus = "completed";
            if($this->s3SyncSettings->syncType === "media"){
                $this->appConfig->canEnableMediaCDN = true;
                $this->appConfig->save();
            }else{
                $this->appConfig->canEnableAssetCDN = true;
                $this->appConfig->save();
            }
            $this->s3SyncSettings->save();
        }
    }


    /**
     * Stop all ongoing synchronization processes.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function stopAllProcesses(): void
    {
        self::resetSyncState(s3SyncSettings: $this->s3SyncSettings);
        wp_send_json_success([
            "message" => esc_html__("Stopped all the running processes.", 'smartsync-lite-media-offloader-and-assets-cdn')
        ]);
    }

    /**
     * Reset the synchronization state to its default values.
     *
     * @param SMARL_S3SyncSettings $s3SyncSettings Settings model;
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public static function resetSyncState(SMARL_S3SyncSettings $s3SyncSettings): void
    {
        $s3SyncSettings->reset();
    }

    /**
     * Synchronize files from the local WordPress file system to AWS S3.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function syncLocalToAws(): void{


        check_ajax_referer('smarl_nonce', 'nonce');

        self::resetSyncState(s3SyncSettings: $this->s3SyncSettings);
        $type = isset($_POST['type']) ? sanitize_text_field(wp_unslash($_POST['type'])) : '';


        $extensions = SMARL_Helper::getExtension(type: $type);

        if (!$this->appConfig) {
            $this->noConfigurationFound();
        }

        try {
            // Initialize S3 client for validation
            $s3 = $this->appConfig->getS3Client(region: "us-east-1");

            if($this->appConfig->serviceProvider === "aws"){
//                $s3->headBucket(['Bucket' => $this->appConfig->bucket]);
            }elseif ($this->appConfig->serviceProvider === "digitalocean") {
                $s3->headBucket(['Bucket' => $this->appConfig->bucket]);
            }

            $uploads_path = wp_upload_dir();
            $uploads_dir = $uploads_path['basedir'];

            $all_files = SMARL_Helper::updateLocalFilesDB(uploads_dir: $uploads_dir, extensions: $extensions);

            // Initialize sync state
            $this->s3SyncSettings->s3SyncSuccesses = [];
            $this->s3SyncSettings->s3SyncErrors = [];
            $this->s3SyncSettings->syncState = 'syncing';
            $this->s3SyncSettings->s3SyncStatus = 'processing';
            $this->s3SyncSettings->s3SyncProcessedFiles = 0;
            $this->s3SyncSettings->s3SyncDirection = 'upload';
            $this->s3SyncSettings->s3SyncTotalFiles = count($all_files);
            $this->s3SyncSettings->s3SyncAllFiles = $all_files;
            $this->s3SyncSettings->extensions = $extensions;
            $type = isset($_POST['type']) ? sanitize_text_field(wp_unslash($_POST['type'])) : '';


            $this->s3SyncSettings->syncType = sanitize_text_field($type);
            $this->s3SyncSettings->save();

            // Schedule first batch
            as_schedule_single_action(time(), 'process_s3_sync_batch', [
                'batch_number' => 1,
                'settings' => [],
                'files' => [],
                'direction' => 'upload',
            ]);

            wp_send_json_success([
                'message' => esc_html__('Sync started', 'smartsync-lite-media-offloader-and-assets-cdn'),
                'total_files' => count($all_files)
            ]);

        } catch (Exception $e) {
            wp_send_json_error(  $e->getMessage());
        }

    }

    /**
     * Toggle the use of a CDN for media or assets.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function toggleCdn(): void
    {
        try{

            check_ajax_referer('smarl_nonce', 'nonce');

            $type = isset($_POST['type']) ? sanitize_text_field(wp_unslash($_POST['type'])) : '';

            $useCdn = isset($_POST['useCdn']) ? filter_var(sanitize_text_field(wp_unslash($_POST['useCdn'])), FILTER_VALIDATE_BOOLEAN) : false;

            if ($type == "media") {
                if($this->appConfig->canEnableMediaCDN){
                    $this->appConfig->useCdnForMedia = $useCdn;
                    $this->appConfig->save();
                }else{
                    $this->appConfig->useCdnForMedia = false;
                    $this->appConfig->save();
                    wp_send_json_error(esc_html__("Please sync all the media before enabling CDN for media", 'smartsync-lite-media-offloader-and-assets-cdn'));
                }

            } elseif ($type == "assets") {

                if($this->appConfig->canEnableAssetCDN){
                    $this->appConfig->useCdnForAssets = $useCdn;
                    $this->appConfig->save();
                }else{
                    $this->appConfig->useCdnForAssets = false;
                    $this->appConfig->save();
                    wp_send_json_error(esc_html__("Please sync all the media before enabling CDN for Assets", 'smartsync-lite-media-offloader-and-assets-cdn'));
                }
            }
            wp_send_json_success([
                "message" => esc_html__("CDN setting updated successfully.", 'smartsync-lite-media-offloader-and-assets-cdn')
            ]);
        }catch (Exception $e){
            wp_send_json_error($e->getMessage());
        }
    }

    public function toggleMediaAutoDelete(): void
    {
        try{
            check_ajax_referer('smarl_nonce', 'nonce');

            $state = isset($_POST['state']) && filter_var(sanitize_text_field(wp_unslash($_POST['state'])), FILTER_VALIDATE_BOOLEAN);

            $this->appConfig->mediaAutoDelete = $state;
            $this->appConfig->save();

            wp_send_json_success([
                "message" => esc_html__("CDN setting updated successfully.", 'smartsync-lite-media-offloader-and-assets-cdn')
            ]);
        }catch (Exception $e){
            wp_send_json_error($e->getMessage());
        }
    }

    /**
     * Clear the AWS CloudFront cache for the specified paths.
     *
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function clearCloudFrontCache(): void{
        check_ajax_referer('smarl_nonce', 'nonce');
        $config = SMARL_CloudConfig::load();
        if (!$config) {
            $this->noConfigurationFound();

        }
        $paths = ["/*"];
        try {
            $this->purgeCloudFrontCache(paths: $paths);
            wp_send_json_success(esc_html__('Cache cleared successfully', 'smartsync-lite-media-offloader-and-assets-cdn'));
        }catch (Exception $e){
            wp_send_json_error($e->getMessage());
        }
    }

    /**
     * Purge the CloudFront cache by invalidating the specified paths.
     *
     * @param array $paths The paths to be invalidated in the cache.
     * @throws Exception If cache invalidation fails.
     * @return void
     * @developer Jay Parmar <infinitie.jay@gmail.com>
     */
    public function purgeCloudFrontCache(array $paths): void
    {

            // Create CloudFront client
            $client = $this->appConfig->getCloudfrontClient();

            // Invalidate cache
             $client->createInvalidation([
                'DistributionId' => $this->appConfig->distributionId,
                'InvalidationBatch' => [
                    'CallerReference' => uniqid(),
                    'Paths' => [
                        'Quantity' => count($paths),
                        'Items' => $paths,
                    ],
                ],
            ]);


    }

    public function noConfigurationFound(): void
    {
        wp_send_json_error(esc_html__('AWS configuration not found.', 'smartsync-lite-media-offloader-and-assets-cdn'));
    }

}
