<?php

namespace App\Jobs;

use App\Enums\BuildStatus;
use App\Models\BuildOrder;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Process\Process;
use Throwable;
use ZipArchive;

class ProcessBuild implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 3;

    public $backoff = [120, 240, 480];

    public $maxExceptions = 3;

    public $timeout = 3600;

    public function middleware(): array
    {
        return [(new WithoutOverlapping($this->buildOrderId))->releaseAfter(300)];
    }

    private const FILE_TYPE_APK = 'apk';

    private const FILE_TYPE_AAB = 'aab';

    private const FILE_TYPE_ANDROID_TXT = 'android-txt';

    private const FILE_TYPE_IOS_TXT = 'ios-txt';

    private const FILE_TYPE_RUNNER = 'runner';

    public function __construct(public int $buildOrderId) {}

    /**
     * @throws Throwable
     */
    public function handle(): void
    {
        $buildDir = null;
        $buildOrder = null;

        try {
            Log::info('=============================================================================================================');
            Log::info('Build process starting :: ', ['buildOrderId' => $this->buildOrderId]);

            set_time_limit(3600);
            ini_set('memory_limit', '2G'); // safer memory limit

            $appzaApiUrl = config('app.appza_api_url');
            if (empty($appzaApiUrl)) {
                throw new \RuntimeException('API URL not configured');
            }

            $buildOrder = BuildOrder::find($this->buildOrderId);
            if (! $buildOrder) {
                Log::error("Build Order :: #{$this->buildOrderId} not found. Job terminated.");

                return;
            }

            if ($buildOrder->status !== BuildStatus::Pending) {
                Log::info("Ignoring Build Order :: #{$buildOrder->id}, already in {$buildOrder->status->value} status.");

                return;
            }

            Log::info('Initial configuration is completed :: ', [
                'buildOrderId' => $buildOrder->id,
                'target' => $buildOrder->build_target,
                'attempt' => $this->attempts(),
                'maxAttempts' => $this->tries,
            ]);

            //            $buildOrder->update(['process_start' => DB::raw('NOW()'), 'status' => BuildStatus::Processing]);
            $buildOrder->update([
                'process_start' => now(),
                'status' => BuildStatus::Processing,
            ]);

            $this->validateBuildRequirements($buildOrder);

            Log::info('....................');
            $buildDir = $this->prepareBuildDir($buildOrder);

            if ($buildOrder->build_target === 'android') {
                Log::info('....................');
                $this->processAndroidBuild($buildDir, $buildOrder);

                Log::info('....................');
                Log::info('Uploading Android build artifacts :: ', ['buildOrderId' => $buildOrder->id]);
                $apkUrl = $this->uploadFileIntoR2($buildDir, 'android-apk', $buildOrder, self::FILE_TYPE_APK);
                $aabUrl = $this->uploadFileIntoR2($buildDir, 'android-aab', $buildOrder, self::FILE_TYPE_AAB);
                $txtUrl = $this->uploadFileIntoR2($buildDir, 'android-build-output', $buildOrder, self::FILE_TYPE_ANDROID_TXT);

                $buildOrder->update([
                    'apk_url' => $apkUrl,
                    'aab_url' => $aabUrl,
                    'android_output_url' => $txtUrl,
                ]);
            } elseif ($buildOrder->build_target === 'ios') {
                Log::info('....................');
                Log::info('Starting iOS build :: ', ['buildOrderId' => $buildOrder->id]);
                $this->processIosBuild($buildDir, $buildOrder);

                Log::info('....................');
                Log::info('Uploading iOS build artifacts :: ', ['buildOrderId' => $buildOrder->id]);
                $txtUrl = $this->uploadFileIntoR2($buildDir, 'ios-build-output', $buildOrder, self::FILE_TYPE_IOS_TXT);

                $buildOrder->update([
                    'ios_output_url' => $txtUrl,
                ]);

            } else {
                throw new \InvalidArgumentException("Unsupported target: {$buildOrder->build_target}");
            }

            Log::info('....................');
            $this->finalizeBuildFiles($buildDir, $buildOrder);

            $parts = explode('/', $buildDir);
            $buildOrder->update([
                'status' => BuildStatus::Completed,
                'build_dir' => implode('/', array_slice($parts, -2)),
                'is_build_dir_delete' => false,
            ]);

            Log::info('....................');
            Log::info('Sending success mail notification :: ', ['buildOrderId' => $buildOrder->id]);
            $this->sendBuildNotification($buildOrder);

            Log::info('....................');
            Log::info('Build complete successfully. ');
        } catch (Throwable $e) {
            Log::info('....................');
            Log::error('Process Build failed :: ', [
                'buildOrderId' => $this->buildOrderId,
                'message' => $e->getMessage(),
                'exception' => get_class($e),
                'line' => $e->getLine(),
            ]);

            if (isset($buildDir, $buildOrder)) {
                try {
                    $txtUrl = $buildOrder->build_target === 'android'
                        ? $this->uploadFileIntoR2($buildDir, 'android-build-output', $buildOrder, self::FILE_TYPE_ANDROID_TXT)
                        : $this->uploadFileIntoR2($buildDir, 'ios-build-output', $buildOrder, self::FILE_TYPE_IOS_TXT);

                    $buildOrder->update([
                        $buildOrder->build_target === 'android' ? 'android_output_url' : 'ios_output_url' => $txtUrl,
                    ]);
                } catch (Throwable $uploadException) {
                    Log::error('Failed to upload error logs :: ', ['error' => $uploadException->getMessage()]);
                }

                $this->finalizeBuildFiles($buildDir, $buildOrder);

                $parts = explode('/', $buildDir);
                $buildOrder->update(['status' => BuildStatus::Failed, 'build_dir' => implode('/', array_slice($parts, -2))]);

                Log::info('....................');
                Log::info('Sending fail mail notification :: ', ['buildOrderId' => $buildOrder->id]);
                $this->sendBuildNotification($buildOrder);
            }

            throw $e;
        }
    }

    private function finalizeBuildFiles(?string $buildDir, ?BuildOrder $buildOrder): void
    {
        if (! empty($buildDir) && isset($buildOrder)) {
            try {
                $zipPath = $this->createBuildZip($buildDir, $buildOrder);
                $zipUrl = $this->uploadZipToR2($zipPath, $buildOrder);
                $buildOrder->update(['build_zip_url' => $zipUrl]);
                $this->cleanupBuildDirectory($buildDir);
            } catch (Throwable $zipError) {
                Log::error('ZIP upload/cleanup failed :: ', ['error' => $zipError->getMessage()]);
            }
        }
    }

    private function sendBuildNotification(BuildOrder $buildOrder): void
    {
        $appzaApiUrl = config('app.appza_api_url');
        if (config('app.is_mail_send') && ! empty($appzaApiUrl)) {
            try {
                Http::post($appzaApiUrl."build/response/{$buildOrder->id}", [
                    'build_message' => "Build {$buildOrder->status->value} for Order #{$buildOrder->id} & mail send api call.",
                ]);
                Log::info("Mail notification sent for build :: #{$buildOrder->id}");
            } catch (Throwable $e) {
                Log::error('Mail notification sent failed :: ', ['error' => $e->getMessage()]);
            }
        }
    }

    private function validateBuildRequirements(BuildOrder $buildOrder): void
    {
        $required = [
            'package_name', 'app_name', 'domain', 'base_suffix', 'base_url', 'build_number',
            'icon_url', 'build_plugin_slug', 'app_license_check_url',
        ];

        if ($buildOrder->build_target === 'android') {
            $required = array_merge($required, ['jks_url', 'key_properties_url']);
        }
        if ($buildOrder->build_target === 'ios') {
            $required = array_merge($required, ['issuer_id', 'key_id', 'api_key_url', 'team_id', 'app_identifier']);
        }

        $missing = array_filter($required, fn ($f) => empty($buildOrder->$f));
        if (! empty($missing)) {
            throw new \InvalidArgumentException('Missing required fields: '.implode(', ', $missing));
        }
    }

    /**
     * @throws \Exception
     */
    private function processAndroidBuild(string $buildDir, BuildOrder $buildOrder): void
    {
        Log::debug('Starting Android build :: ', ['buildDir' => $buildDir, 'buildOrderId' => $buildOrder->id]);
        $diskPath = $this->getRelativeDiskPath($buildDir);

        // Download files safely using Laravel HTTP client
        $this->downloadFile($buildOrder->jks_url, "$diskPath/android/app/upload-keystore.jks");
        $this->downloadFile($buildOrder->key_properties_url, "$diskPath/android/key.properties");
        $this->downloadFile($buildOrder->icon_url, "$diskPath/assets/icons/launcher_icon.png");

        if ($buildOrder->splash_screen) {
            $this->downloadFile($buildOrder->splash_screen, "$diskPath/assets/icons/splash_screen.png");
        }
        if ($buildOrder->is_push_notification) {
            $this->downloadFile($buildOrder->android_push_notification_url, "$diskPath/android/app/google-services.json");
        }

        $this->runBuildCommand($buildDir, $buildOrder);
    }

    /**
     * @throws \Exception
     */
    private function processIosBuild(string $buildDir, BuildOrder $buildOrder): void
    {
        Log::info('Processing iOS build :: ', ['buildDir' => $buildDir, 'buildOrderId' => $buildOrder->id]);
        $diskPath = $this->getRelativeDiskPath($buildDir);
        $keyFile = "AuthKey_{$buildOrder->key_id}.p8";

        $this->downloadFile($buildOrder->api_key_url, "$diskPath/ios/{$keyFile}");
        $this->downloadFile($buildOrder->icon_url, "$diskPath/assets/icons/launcher_icon.png");

        if ($buildOrder->splash_screen) {
            $this->downloadFile($buildOrder->splash_screen, "$diskPath/assets/icons/splash_screen.png");
        }
        if ($buildOrder->is_push_notification) {
            $this->downloadFile($buildOrder->ios_push_notification_url, "$diskPath/ios/Runner/GoogleService-Info.plist");
        }

        $this->runBuildCommand($buildDir, $buildOrder, ['key-filepath' => $keyFile]);
    }

    /**
     * @throws ConnectionException
     * @throws \Exception
     */
    private function downloadFile(string $url, string $savePath): void
    {
        if (! filter_var($url, FILTER_VALIDATE_URL)) {
            throw new \Exception("Invalid URL: $url");
        }

        $response = Http::timeout(30)->retry(3, 1000)->get($url);
        if ($response->failed()) {
            throw new \Exception("Failed to download file: $url");
        }

        Storage::disk('builds')->put($savePath, $response->body());
    }

    /**
     * @throws \Exception
     */
    private function prepareBuildDir(BuildOrder $buildOrder): string
    {
        Log::info('Preparing build directory :: ', ['buildOrderId' => $buildOrder->id]);
        $buildDir = $this->getTargetDir($buildOrder);

        $templates = [
            'wordpress' => config('app.build_template_for_woo'),
            'woocommerce' => config('app.build_template_for_woo'),
            'tutor-lms' => config('app.build_template_for_tutor_lms'),
            'fluent-community' => config('app.build_template_for_fluent_community'),
        ];

        if (! isset($templates[$buildOrder->build_plugin_slug])) {
            throw new \Exception("Unknown plugin: {$buildOrder->build_plugin_slug}");
        }

        $templateDir = $templates[$buildOrder->build_plugin_slug];
        if (! File::isReadable($templateDir)) {
            throw new \Exception("Template dir not readable: $templateDir");
        }

        if (File::isDirectory($buildDir)) {
            File::deleteDirectory($buildDir);
        }
        File::copyDirectory($templateDir, $buildDir);
        Log::info('Copying template directory :: ', ['from' => $templateDir, 'to' => $buildDir]);
        chmod($buildDir.'/build_flow.sh', 0755);

        return $buildDir;
    }

    /**
     * @throws \Exception
     */
    private function runBuildCommand(string $buildDir, BuildOrder $buildOrder, array $extra = []): void
    {
        $command = $this->makeBuildCommand($buildOrder, $extra);
        Log::info('Running build command :: ', ['command' => $command, 'buildOrderId' => $buildOrder->id]);

        $process = Process::fromShellCommandline($command, $buildDir);
        $process->setTimeout($this->timeout);
        $process->run();

        $diskPath = $this->getRelativeDiskPath($buildDir);
        Storage::disk('builds')->put($diskPath.'/build_output.txt', $process->getOutput().$process->getErrorOutput());

        if (! $process->isSuccessful()) {
            Log::error('Build command failed :: ', [
                'returnCode' => $process->getExitCode(),
                'buildOrderId' => $buildOrder->id,
                'outputLines' => count($process->getOutput()),
            ]);
            $txtUrl = $buildOrder->build_target === 'android'
                ? $this->uploadFileIntoR2($buildDir, 'android-build-output', $buildOrder, self::FILE_TYPE_ANDROID_TXT)
                : $this->uploadFileIntoR2($buildDir, 'ios-build-output', $buildOrder, self::FILE_TYPE_IOS_TXT);

            $buildOrder->update([
                $buildOrder->build_target === 'android' ? 'android_output_url' : 'ios_output_url' => $txtUrl,
            ]);

            throw new \RuntimeException("Build failed with return code: {$process->getExitCode()}");
        }

        Log::info('Build command completed successfully :: ', ['buildOrderId' => $buildOrder->id]);
    }

    private function getTargetDir(BuildOrder $buildOrder): string
    {
        $dirName = "{$buildOrder->id}_{$buildOrder->build_target}_".date('Ymd_His');

        return Storage::disk('builds')->path("$dirName");
    }

    private function getRelativeDiskPath(string $path): string
    {
        return str_replace(Storage::disk('builds')->path(''), '', $path);
    }

    private function makeBuildCommand(BuildOrder $buildOrder, array $extra = []): string
    {
        $iosExtra = '';
        $pushNotify = $buildOrder->is_push_notification ? ' --has-push-notification' : '';
        $googleAuth = '';

        if ($buildOrder->is_build_google_auth) {
            $googleAuth = sprintf(
                ' --has-google-login --google-web-client-id %s --google-ios-client-id %s',
                escapeshellarg($buildOrder->google_web_client_id),
                escapeshellarg($buildOrder->google_ios_client_id)
            );
        }

        if ($buildOrder->build_target === 'ios') {
            $iosExtra = sprintf(
                ' --key-id %s --issuer-id %s --key-filepath %s --app-identifier %s --team-id %s',
                escapeshellarg($buildOrder->key_id),
                escapeshellarg($buildOrder->issuer_id),
                escapeshellarg($extra['key-filepath'] ?? ''),
                escapeshellarg($buildOrder->app_identifier),
                escapeshellarg($buildOrder->team_id)
            );
        }

        return sprintf(
            'sh build_flow.sh --app-license-check-url %s --package-name %s --app-name %s --domain %s --base-suffix %s --base-url %s --build-number %d --should-build-%s%s%s%s',
            escapeshellarg($buildOrder->app_license_check_url),
            escapeshellarg($buildOrder->package_name),
            escapeshellarg($buildOrder->app_name),
            escapeshellarg(rtrim($buildOrder->domain, '/').'/'),
            escapeshellarg($buildOrder->base_suffix),
            escapeshellarg($buildOrder->base_url),
            (int) $buildOrder->build_number,
            escapeshellarg($buildOrder->build_target),
            $iosExtra,
            $pushNotify,
            $googleAuth
        );
    }

    /**
     * @throws \Exception
     */
    private function uploadFileIntoR2(
        string $directory,
        string $r2Folder,
        BuildOrder $buildOrder,
        string $filetype
    ): ?string {
        Log::info("{$filetype} file preparing for uploading into R2 :: ", [
            'directory' => $directory,
            'r2Folder' => $r2Folder,
            'filetype' => $filetype,
            'buildOrderId' => $buildOrder->id,
        ]);

        // File extension by type
        $fileExtension = match ($filetype) {
            self::FILE_TYPE_APK => '.apk',
            self::FILE_TYPE_AAB => '.aab',
            self::FILE_TYPE_ANDROID_TXT,
            self::FILE_TYPE_IOS_TXT => '.txt',
            self::FILE_TYPE_RUNNER => '.log',
            default => throw new \InvalidArgumentException("Unsupported file type: {$filetype}"),
        };

        // Find file
        $filePath = $this->getFileByExtension($directory, $fileExtension);

        if (! $filePath || ! file_exists($filePath)) {
            Log::error("{$filetype} File not found :: ", [
                'directory' => $directory,
                'extension' => $fileExtension,
                'buildOrderId' => $buildOrder->id,
            ]);

            return null;
        }

        // Wait until file is fully written (APK, AAB, TXT, LOG)
        if (! $this->waitForFileReady($filePath)) {
            Log::error("{$filetype} File not stable yet :: ", [
                'filePath' => $filePath,
                'buildOrderId' => $buildOrder->id,
            ]);

            return null;
        }

        try {
            // Build file name
            $appName = $this->getAppNameFromPackage($buildOrder->package_name);
            $extension = pathinfo($filePath, PATHINFO_EXTENSION);
            $timestamp = now()->format('Ymd_His');

            $newFileName = "{$appName}_build_{$buildOrder->build_number}_{$timestamp}.{$extension}";
            $r2Path = "{$r2Folder}/{$newFileName}";

            // Content type
            $contentType = match ($extension) {
                'apk', 'aab' => 'application/vnd.android.package-archive',
                'txt', 'log' => 'text/plain',
                default => 'application/octet-stream',
            };

            Log::info("{$filetype} file size :: ", [
                'filePath' => $filePath,
                'size_kb' => round(filesize($filePath) / 1024, 2),
                //                'contentType' => $contentType,
                //                'r2Path' => $r2Path,
            ]);

            // Retry upload (for ALL file types)
            $attempts = 3;

            while ($attempts--) {
                try {
                    $success = Storage::disk('r2')->putFileAs(
                        $r2Folder,
                        new \Illuminate\Http\File($filePath), // STREAMING
                        $newFileName,
                        [
                            'visibility' => 'public',
                            'Content-Type' => $contentType,
                        ]
                    );

                    if ($success) {
                        Log::info("{$filetype} file upload into R2 complete :: ", [
                            'r2Path' => $r2Path,
                            'buildOrderId' => $buildOrder->id,
                        ]);

                        return rtrim(config('app.image_public_path'), '/').'/'.$r2Path;
                    }
                } catch (\Throwable $e) {
                    Log::error("{$filetype} file upload attempt failed :: ", [
                        'attempts_left' => $attempts,
                        'error' => $e->getMessage(),
                        'filePath' => $filePath,
                    ]);

                    if ($attempts === 0) {
                        throw $e;
                    }

                    sleep(1);
                }
            }
        } catch (\Throwable $e) {
            Log::error("{$filetype} file upload attempt failed :: ", [
                'error' => $e->getMessage(),
                'filePath' => $filePath,
                'buildOrderId' => $buildOrder->id,
            ]);
        }

        return null;
    }

    private function waitForFileReady(string $path, int $checks = 3, int $sleep = 1): bool
    {
        $lastSize = 0;

        for ($i = 0; $i < $checks; $i++) {
            clearstatcache(true, $path);
            $currentSize = filesize($path);

            if ($currentSize > 0 && $currentSize === $lastSize) {
                return true;
            }

            $lastSize = $currentSize;
            sleep($sleep);
        }

        return false;
    }

    private function getFileByExtension(string $dir, string $ext): ?string
    {
        $files = glob("$dir/*$ext");

        return $files[0] ?? null;
    }

    private function getAppNameFromPackage(string $package): string
    {
        $parts = explode('.', $package);

        return end($parts) ?: 'app';
    }

    /* =====================================================
     | ZIP FIX (IOS SAFE)
     ===================================================== */

    private function createBuildZip(string $buildDir, BuildOrder $buildOrder): string
    {
        $zipPath = "{$buildDir}/build_{$buildOrder->build_target}_{$buildOrder->id}.zip";
        $password = config('app.build_zip_password');

        Log::info('ZIP creation started :: ', [
            'buildDir' => $buildDir,
            'zipPath' => $zipPath,
            'target' => $buildOrder->build_target,
        ]);

        $zip = new ZipArchive;
        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            throw new \RuntimeException('Cannot create zip file');
        }

        $zip->setPassword($password);

        /**
         * Files & folders to include (ROOT)
         */
        $allowed = [
            'lib',
            'assets',
            'android',
            'ios',
            'pubspec.yaml',
            '.fvmrc',
            'analysis_options.yaml',
            'build_flow.sh',
            'refresh_the_engine.sh',
        ];

        foreach ($allowed as $item) {
            $fullPath = "{$buildDir}/{$item}";

            if (! file_exists($fullPath)) {
                continue;
            }

            if (is_dir($fullPath)) {
                $this->zipDirectoryFiltered(
                    zip: $zip,
                    sourceDir: $fullPath,
                    zipRoot: $item,
                    password: $password,
                    buildTarget: $buildOrder->build_target
                );
            } else {
                $zip->addFile($fullPath, $item);
                $zip->setEncryptionName($item, ZipArchive::EM_AES_256);
            }
        }

        $zip->close();

        Log::info('ZIP created successfully :: ', [
            'zipPath' => $zipPath,
            'buildOrderId' => $buildOrder->id,
        ]);

        return $zipPath;
    }

    private function zipDirectoryFiltered(
        ZipArchive $zip,
        string $sourceDir,
        string $zipRoot,
        string $password,
        string $buildTarget
    ): void {
        $basePath = realpath($sourceDir);
        if (! $basePath) {
            return;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($basePath, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isLink()) {
                continue;
            }

            $realPath = $file->getRealPath();
            if (! $realPath) {
                continue;
            }

            $relativePath = ltrim(str_replace($basePath, '', $realPath), DIRECTORY_SEPARATOR);
            $zipPath = $zipRoot.'/'.$relativePath;

            /**
             * ---------------------------------------
             * iOS CLEANUP RULES (ONLY FOR IOS BUILDS)
             * ---------------------------------------
             */
            if ($buildTarget === 'ios') {
                if (
                    str_starts_with($zipPath, 'ios/Pods/') ||
                    str_starts_with($zipPath, 'ios/.symlinks/') ||
                    str_starts_with($zipPath, 'ios/build/') ||
                    str_ends_with($zipPath, '.ipa') ||
                    str_ends_with($zipPath, '.dSYM') ||
                    str_ends_with($zipPath, 'ios/Runner.app.dSYM.zip')
                ) {
                    continue;
                }
            }

            if ($file->isDir()) {
                $zip->addEmptyDir($zipPath);
            } else {
                $zip->addFile($realPath, $zipPath);
                $zip->setEncryptionName($zipPath, ZipArchive::EM_AES_256);
            }
        }
    }

    private function uploadZipToR2(string $zipPath, BuildOrder $buildOrder): string
    {
        $r2Path = 'build-zips/'.basename($zipPath);
        Storage::disk('r2')->putFileAs('build-zips', $zipPath, basename($r2Path), ['visibility' => 'public']);
        Log::info("Zip upload complete :: $buildOrder->id");

        return config('app.image_public_path').$r2Path;
    }

    private function cleanupBuildDirectory(?string $buildDir): void
    {
        if (! empty($buildDir) && File::isDirectory($buildDir)) {
            Log::info('Cleaning up build directory :: ', ['buildDir' => $buildDir]);
            try {
                File::deleteDirectory($buildDir);
            } catch (\Exception $e) {
                Log::error('Failed to clean up build directory :: ', [
                    'buildDir' => $buildDir,
                    'error' => $e->getMessage(),
                ]);
            }
        }
    }
}
