<?php
declare(strict_types=1);

namespace App\Lib;

use Aws\S3\S3Client;
use Aws\MediaConvert\MediaConvertClient;
use Aws\Exception\AwsException;
use Aws\S3\Exception\S3Exception;
use App\Lib\Ffmpeg;
use App\Lib\Utility;

class Aws
{
	private S3Client $s3;
	private static ?Aws $instance = null;

	private function __construct()
	{
		$iamKey = IAM_KEY;
		$iamSecret = IAM_SECRET;
		$this->s3 = new S3Client([
			'credentials' => ['key' => $iamKey, 'secret' => $iamSecret],
			'version'     => 'latest',
			'region'      => S3_REGION,
		]);
	}

	public static function getInstance(): Aws
	{
		if (self::$instance === null) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	public function s3_video_upload(int $userId, array $param, array $soundDetails, array $videoDetails, bool $duet): array
	{
		$original = Utility::uploadOriginalVideoFileIntoTemporaryFolder($param, $userId);
		if ($original['error'] > 0) {
			echo json_encode(['code' => 201, 'msg' => $original['msg']]);
			exit;
		}
		$finalVideo = $original['msg'];
		if (!empty($videoDetails)) {
			$finalVideo = Ffmpeg::duet($finalVideo, $videoDetails['Video']['video'], $duet);
		}
		$gif         = Ffmpeg::videoToGif($finalVideo, $userId);
		$thumb       = Ffmpeg::videoToThumb($finalVideo, $userId);
		$thumbSmall  = Utility::cropImage($thumb);

		if (empty($soundDetails)) {
			$mp3      = Ffmpeg::convertVideoToAudio($finalVideo, $userId);
			$mp3Name  = $mp3 ? 'audio/' . basename($mp3) : '';
			$output['audio'] = $mp3Name;
			$final    = $finalVideo;
		} else {
			$final         = Ffmpeg::mergeVideoWithSound($finalVideo, $soundDetails['Sound']['audio']);
			$output['audio'] = '';
		}

		$videoName      = 'video/' . basename($final);
		$gifName        = 'gif/' . basename($gif);
		$thumbName      = 'thum/' . basename($thumb);
		$thumbSmallName = 'thum/' . basename($thumbSmall);

		try {
			$uploadVideo      = $this->s3->putObject(['Bucket' => BUCKET_NAME, 'Key' => $videoName,      'ACL' => 'public-read', 'SourceFile' => $final,      'StorageClass' => 'REDUCED_REDUNDANCY']);
			$uploadGif        = $this->s3->putObject(['Bucket' => BUCKET_NAME, 'Key' => $gifName,        'ACL' => 'public-read', 'SourceFile' => $gif,        'StorageClass' => 'REDUCED_REDUNDANCY']);
			$uploadThumb      = $this->s3->putObject(['Bucket' => BUCKET_NAME, 'Key' => $thumbName,      'ACL' => 'public-read', 'SourceFile' => $thumb,      'StorageClass' => 'REDUCED_REDUNDANCY']);
			$uploadThumbSmall = $this->s3->putObject(['Bucket' => BUCKET_NAME, 'Key' => $thumbSmallName, 'ACL' => 'public-read', 'SourceFile' => $thumbSmall, 'StorageClass' => 'REDUCED_REDUNDANCY']);

			if (!empty($mp3Name)) {
				$uploadMp3              = $this->s3->putObject(['Bucket' => BUCKET_NAME, 'Key' => $mp3Name, 'ACL' => 'public-read', 'SourceFile' => $mp3, 'StorageClass' => 'REDUCED_REDUNDANCY']);
				$finalOutput['audio']   = $uploadMp3['ObjectURL'];
				unlink($mp3);
			} else {
				$finalOutput['audio'] = '';
			}

			$finalOutput['video']      = $uploadVideo['ObjectURL'];
			$finalOutput['gif']        = $uploadGif['ObjectURL'];
			$finalOutput['thum']       = $uploadThumb['ObjectURL'];
			$finalOutput['thum_small'] = $uploadThumbSmall['ObjectURL'];

			Utility::unlinkFile($final);
			Utility::unlinkFile($gif);
			Utility::unlinkFile($thumb);
			Utility::unlinkFile($thumbSmall);

			return $finalOutput;
		} catch (S3Exception $e) {
			$msg = $e->getMessage();
			if (str_contains($msg, 'NoSuchBucket')) {
				define('s3_Error', 'NoSuchBucket');
				echo $msg;
			} elseif (str_contains($msg, 'AccessDenied')) {
				define('s3_Error', 'AccessDenied');
				echo $msg;
			}
		} catch (AwsException $e) {
			echo $e->getMessage();
		}
		exit;
	}

	public function fileUploadToS3Multipart(string $file, ?string $ext = null): array
	{
		$random = Utility::random_string(5);
		if ($ext === null) {
			$type = $_FILES[$file]['type'];
			$parts = explode('/', $type);
			$ext   = $parts[1] === 'mpeg' ? 'mp3' : $parts[1];
		}
		$filename = uniqid() . $random . '.' . $ext;
		$folder   = match ($ext) {
			'mp4'       => 'videos/',
			'jpg','jpeg','png','gif' => 'images/',
			'pdf'       => 'pdf/',
			'mp3','mpeg'=> 'audio/',
			default     => '',
		};

		try {
			$multipart  = $this->s3->createMultipartUpload([
				'Bucket'        => BUCKET_NAME,
				'Key'           => $folder . $filename,
				'ACL'           => 'public-read',
				'StorageClass'  => 'REDUCED_REDUNDANCY',
				'ContentType'   => $_FILES[$file]['type'],
				'Metadata'      => ['Cache-Control' => 'max-age=31536000']
			]);
			$uploadId    = $multipart['UploadId'];
			$handle      = fopen($_FILES[$file]['tmp_name'], 'rb');
			$partsList   = [];
			$partNumber  = 1;

			while (!feof($handle)) {
				$body   = fread($handle, 5 * 1024 * 1024);
				$result = $this->s3->uploadPart([
					'Bucket'      => BUCKET_NAME,
					'Key'         => $folder . $filename,
					'UploadId'    => $uploadId,
					'PartNumber'  => $partNumber,
					'Body'        => $body
				]);
				$partsList[] = ['PartNumber' => $partNumber, 'ETag' => $result['ETag']];
				$partNumber++;
			}

			fclose($handle);

			$this->s3->completeMultipartUpload([
				'Bucket'         => BUCKET_NAME,
				'Key'            => $folder . $filename,
				'UploadId'       => $uploadId,
				'MultipartUpload'=> ['Parts' => $partsList]
			]);

			return ['code' => 200, 'msg' => $folder . $filename];
		} catch (S3Exception $e) {
			$msg = str_contains($e->getMessage(), 'NoSuchBucket')
				? 'No such Bucket exist'
				: (str_contains($e->getMessage(), 'AccessDenied')
					? 'Access Denied of aws bucket'
					: 'some invalid error in aws');
			if (str_contains($e->getMessage(), 'NoSuchBucket')) define('s3_Error', 'NoSuchBucket');
			if (str_contains($e->getMessage(), 'AccessDenied'))  define('s3_Error', 'AccessDenied');
			return ['code' => 201, 'msg' => $msg];
		}
	}

	public function fileUploadToS3(string $file, string $ext, bool $folderparam = false): array
	{
		$ext      = pathinfo($file, PATHINFO_EXTENSION);
		$random   = Utility::random_string(5);
		$filename = uniqid() . $random . '.' . $ext;
		$mapping  = [
			'mp3' => ['audio/mpeg', 'audio/'],
			'png' => ['image/png', 'images/'],
			'gif' => ['image/gif', 'profile/'],
			'mp4' => ['video/mp4', 'video/']
		];
		$content  = $mapping[$ext][0] ?? 'image/jpeg';
		$folder   = $folderparam ? ($mapping[$ext][1] ?? 'thum/') : 'profile/';

		try {
			$url = $this->s3->putObject([
				'Bucket'      => BUCKET_NAME,
				'Key'         => $folder . $filename,
				'ACL'         => 'public-read',
				'SourceFile'  => $file,
				'StorageClass'=> 'REDUCED_REDUNDANCY',
				'ContentType' => $content
			])['ObjectURL'];

			return ['code' => 200, 'msg' => $url];
		} catch (S3Exception $e) {
			if (str_contains($e->getMessage(), 'NoSuchBucket')) define('s3_Error', 'NoSuchBucket');
			if (str_contains($e->getMessage(), 'AccessDenied'))  define('s3_Error', 'AccessDenied');
			return ['code' => 201, 'msg' => 'some invalid error in aws'];
		}
	}

	public function fileUpload(string $file, string $ext, bool $folderparam = false): array
	{
		$random   = Utility::random_string(5);
		$filename = uniqid() . $random . '.' . $ext;
		$mapping  = [
			'mp3' => ['audio/mpeg', 'audio/'],
			'png' => ['image/png', 'thum/'],
			'mp4' => ['video/mp4', 'video/']
		];
		$content  = $mapping[$ext][0] ?? 'image/jpeg';
		$folder   = $folderparam ? ($mapping[$ext][1] ?? 'thum/') : 'profile/';

		try {
			$url = $this->s3->putObject([
				'Bucket'      => BUCKET_NAME,
				'Key'         => $folder . $filename,
				'ACL'         => 'public-read',
				'Body'        => $file,
				'StorageClass'=> 'REDUCED_REDUNDANCY',
				'ContentType' => $content
			])['ObjectURL'];

			return ['code' => 200, 'msg' => $url];
		} catch (S3Exception $e) {
			if (str_contains($e->getMessage(), 'NoSuchBucket')) define('s3_Error', 'NoSuchBucket');
			if (str_contains($e->getMessage(), 'AccessDenied'))  define('s3_Error', 'AccessDenied');
			return ['code' => 201, 'msg' => 'some invalid error in aws'];
		}
	}

	public function testFileUploadToS3(string $file): array
	{
		try {
			$url = $this->s3->putObject([
				'Bucket'      => BUCKET_NAME,
				'Key'         => 'thum/test.png',
				'ACL'         => 'public-read',
				'SourceFile'  => $file,
				'StorageClass'=> 'REDUCED_REDUNDANCY',
				'ContentType' => 'image/jpeg'
			])['ObjectURL'];

			return ['code' => 200, 'msg' => $url];
		} catch (S3Exception $e) {
			if (str_contains($e->getMessage(), 'NoSuchBucket')) define('s3_Error', 'NoSuchBucket');
			if (str_contains($e->getMessage(), 'AccessDenied'))  define('s3_Error', 'AccessDenied');
			return ['code' => 201, 'msg' => str_contains($e->getMessage(), 'NoSuchBucket') ? 'No such Bucket exist' : 'Access Denied of aws bucket'];
		}
	}

	public function profileImageToS3(string $file, string $extension): array
	{
		$random   = Utility::random_string(5);
		$filename = uniqid() . $random . '.' . $extension;
		$type     = $extension === 'png' ? 'image/png' : 'application/octet-stream';

		try {
			$url = $this->s3->putObject([
				'Bucket'      => BUCKET_NAME,
				'Key'         => "profile/{$filename}",
				'ACL'         => 'public-read',
				'SourceFile'  => $file,
				'StorageClass'=> 'REDUCED_REDUNDANCY',
				'ContentType' => $type
			])['ObjectURL'];

			return ['code' => 200, 'msg' => $url];
		} catch (S3Exception $e) {
			if (str_contains($e->getMessage(), 'NoSuchBucket')) define('s3_Error', 'NoSuchBucket');
			if (str_contains($e->getMessage(), 'AccessDenied'))  define('s3_Error', 'AccessDenied');
			return ['code' => 201, 'msg' => 'No such Bucket exist'];
		}
	}

	public function addSticker(string $file, string $extension): array
	{
		return $this->profileImageToS3($file, $extension);
	}

	public function deleteObjectS3(string $url): bool
	{
		$parts = explode('/', $url);
		if (count($parts) < 2) {
			return false;
		}
		$key = $parts[count($parts) - 2] . '/' . $parts[count($parts) - 1];
		try {
			$res = $this->s3->deleteObject(['Bucket' => BUCKET_NAME, 'Key' => $key]);
			return (bool)$res->get('DeleteMarker');
		} catch (S3Exception $e) {
			return false;
		}
	}

	public function renameS3File(string $newKey, string $oldKey): bool
	{
		try {
			$this->s3->copyObject(['Bucket' => BUCKET_NAME, 'CopySource' => BUCKET_NAME . '/video/' . $newKey, 'Key' => 'video/' . $oldKey]);
			$this->s3->deleteObject(['Bucket' => BUCKET_NAME, 'Key' => 'video/' . $newKey]);
			return true;
		} catch (AwsException $e) {
			return false;
		}
	}

	public function s3FilePermission(string $key): void
	{
		$this->s3->putObject(['Bucket' => BUCKET_NAME, 'Key' => $key, 'ACL' => 'public-read']);
	}

	public static function transcodeVideo(string $name): array
	{
		$credentials = new \Aws\Credentials\Credentials(IAM_KEY, IAM_SECRET);
		$mediaConvert = new MediaConvertClient(['version' => 'latest', 'region' => S3_REGION, 'credentials' => $credentials]);
		$settings = [
			'Role' => 'arn:aws:iam::730335416723:role/service-role/MediaConvert_Default_Role',
			'Settings' => [
				'TimecodeConfig' => ['Source' => 'ZEROBASED'],
				'OutputGroups' => [[
					'Name' => 'File Group',
					'Outputs' => [[
						'ContainerSettings' => [
							'Container' => 'MP4',
							'Mp4Settings' => ['CslgAtom' => 'INCLUDE', 'FreeSpaceBox' => 'EXCLUDE', 'MoovPlacement' => 'PROGRESSIVE_DOWNLOAD']
						],
						'VideoDescription' => [
							'CodecSettings' => [
								'Codec' => 'H_264',
								'H264Settings' => ['MaxBitrate' => 5000000, 'QualityTuningLevel' => 'MULTI_PASS_HQ', 'QvbrSettings' => ['QvbrQualityLevel' => 8], 'RateControlMode' => 'QVBR', 'SceneChangeDetect' => 'TRANSITION_DETECTION']
							]
						],
						'AudioDescriptions' => [[
							'CodecSettings' => ['Codec' => 'AAC', 'AacSettings' => ['Bitrate' => 96000, 'CodingMode' => 'CODING_MODE_2_0', 'SampleRate' => 48000]]
						]],
						'NameModifier' => '_1',
					]]
				]],
				'FollowSource' => 1,
				'Inputs' => [[
					'AudioSelectors' => ['Audio Selector 1' => ['DefaultSelection' => 'DEFAULT']],
					'VideoSelector' => ['Rotate' => 'AUTO'],
					'TimecodeSource' => 'ZEROBASED',
					'FileInput' => "s3://foodtok/video/{$name}"
				]]
			],
			'BillingTagsSource' => 'JOB',
			'AccelerationSettings' => ['Mode' => 'DISABLED'],
			'StatusUpdateInterval' => 'SECONDS_60',
			'Priority' => 0
		];

		try {
			$result = $mediaConvert->createJob($settings);
			return ['code' => 200, 'msg' => $result['Job']['Id']];
		} catch (AwsException $e) {
			return ['code' => 201, 'msg' => $e->getMessage()];
		}
	}

	public static function getVideoRotationInfo(string $filepath): ?string
	{
		$cmd = "ffprobe -v quiet -select_streams v:0 -show_streams " . escapeshellarg($filepath) . "|grep -i rotation=";
		$out = exec($cmd);
		return $out !== null ? trim($out) : null;
	}

	public static function getVideoResolution(string $filePath): array
	{
		$cmd    = "ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 " . escapeshellarg($filePath);
		$output = shell_exec($cmd);
		[$width, $height] = explode('x', trim($output));
		return ['width' => (int)$width, 'height' => (int)$height];
	}

	public function transcodeAudio(string $name): array
	{
		$credentials  = new \Aws\Credentials\Credentials(IAM_KEY, IAM_SECRET);
		$mediaConvert = new MediaConvertClient(['version' => 'latest', 'region' => S3_REGION, 'credentials' => $credentials]);
		$settings = [
			'Role' => 'arn:aws:iam::730335416723:role/service-role/MediaConvert_Default_Role',
			'Settings' => [
				'TimecodeConfig' => ['Source' => 'ZEROBASED'],
				'OutputGroups' => [[
					'Name' => 'File Group',
					'Outputs' => [[
						'ContainerSettings' => ['Container' => 'RAW'],
						'AudioDescriptions' => [[
							'AudioSourceName' => 'Audio Selector 1',
							'CodecSettings' => ['Codec' => 'AAC', 'AacSettings' => ['Bitrate' => 96000, 'CodingMode' => 'CODING_MODE_2_0', 'SampleRate' => 48000]]
						]],
						'Extension' => 'mp3',
						'NameModifier' => '1'
					]]
				]],
				'FollowSource' => 1,
				'Inputs' => [[
					'AudioSelectors' => ['Audio Selector 1' => ['DefaultSelection' => 'DEFAULT']],
					'TimecodeSource' => 'ZEROBASED',
					'FileInput' => "s3://foodtok/audio/{$name}"
				]]
			],
			'BillingTagsSource'      => 'JOB',
			'AccelerationSettings'   => ['Mode' => 'DISABLED'],
			'StatusUpdateInterval'   => 'SECONDS_60',
			'Priority'               => 0
		];

		try {
			$result = $mediaConvert->createJob($settings);
			return ['code' => 200, 'msg' => $result['Job']['Id']];
		} catch (AwsException $e) {
			return ['code' => 201, 'msg' => $e->getMessage()];
		}
	}
}
