import { RequestQuery } from '@hapi/hapi'
import * as Path from 'path';
import * as Fs from 'file-system';
import FsPromises from 'fs/promises';
import { finished } from 'stream/promises';
import * as uuid from 'uuid';
import ffmpeg from 'fluent-ffmpeg';
import sharp from 'sharp';
import * as Constants from '../config/constants';
import { AttachmentDao } from "../dao/attachment.dao"
import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectsCommand } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Readable } from 'stream';
import { AppError } from "../../utils/errors";
import { Common } from '../../utils/common';
import Axios from 'axios'
import FormData from "form-data";

const s3Client = new S3Client({
    region: process.env.S3_REGION!,
    credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY!,
        secretAccessKey: process.env.S3_ACCESS_SECRET!,
    }
});


const imageFileExtensions = ['.jpeg', '.png', '.jpg', '.svg'];
const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.wmv', '.m4v', '.ts', '.3gp']
const tempDir = './resources/tmp';

export class AttachmentService {
    private accountId: number | null;
    private userId: number | null;
    private language: string;
    private scope: string[] | null;
    private config: userConfig | null;
    private attachmentDao: AttachmentDao;
    constructor(
        private options: {
            language: string;
            scope: string[] | null;
            accountId: number | null;
            userId: number | null;
            config: userConfig | null;
        } = {
                language: process.env.DEFAULT_LANGUAGE_CODE!,
                scope: null,
                config: null,
                userId: null,
                accountId: null,
            }
    ) {
        this.language = options.language;
        this.scope = options.scope ?? [];
        this.config = options.config ?? null;
        this.userId = options.userId ?? null;
        this.accountId = options.accountId ?? null;
        this.attachmentDao = new AttachmentDao({
            userId: this.userId,
            accountId: this.accountId,
            language: this.language,
            scope: this.scope,
            config: this.config
        });
    }
    // checkIf image exists
    verifyFile = async (ids: number[]): Promise<attachmentObjectInteface[]> => {
        try {
            const verifyFilesInput: AttachmentVerifyFilesDaoInput = { ids };
            let existence = await this.attachmentDao.verifyFiles(verifyFilesInput);
            return existence;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // get temporary location
    private getTempVideoPath = (sourcePath: string): string => {
        try {
            const timestamp = Date.now();
            const extension = Path.extname(sourcePath);
            const tempVideoFile = `video-${timestamp}${extension}`;
            const tempVideoPath = Path.join(tempDir, tempVideoFile);
            return tempVideoPath;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // get file from s3 to temp location for processing
    private getFileFromS3 = async (sourcePath: string, bucketName: string): Promise<string> => {
        try {
            const command = new GetObjectCommand({ Bucket: bucketName, Key: sourcePath });
            const s3Object = await s3Client.send(command);
            const tempPath = this.getTempVideoPath(sourcePath);
            const stream = s3Object.Body as Readable;
            const buffer = await this.streamToBuffer(stream);
            await FsPromises.writeFile(tempPath, buffer);
            return tempPath;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }


    // get file from s3 to temp location for processing
    private moveFileToS3 = async (sourcePath: string, destinationPath: string, bucketName: string): Promise<boolean> => {
        try {
            const stream = Fs.createReadStream(sourcePath);
            let buffer = await this.streamToBuffer(stream);
            // Save original as WebP
            const webpOriginalBuffer = await sharp(buffer).webp({ lossless: true }).toBuffer();
            const webpOriginalKey = sourcePath.replace(/\.[^/.]+$/, '.webp');
            
            const upload = new Upload({
                client: s3Client,
                params: {
                    Bucket: bucketName,
                    Key: webpOriginalKey,
                    Body: webpOriginalBuffer,
                }
            });
            await upload.done();
            return true;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }


    // Utility to convert stream to buffer
    private streamToBuffer = async (stream: NodeJS.ReadableStream): Promise<Buffer> => {
        try {
            return new Promise((resolve, reject) => {
                const chunks: Buffer[] = [];
                stream.on('data', chunk => chunks.push(Buffer.from(chunk)));
                stream.on('error', reject);
                stream.on('end', () => resolve(Buffer.concat(chunks)));
            });
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // generate thumbnails
    private generateThumbnails = async (sourcePath: string, extension: string, bucketName?: string): Promise<boolean> => {
        try {
            if (!process.env.THUMB_NAIL_OPTIONS) return true;
            const sizeArray = process.env.THUMB_NAIL_OPTIONS.split(',').filter(Boolean);
            let originalBuffer: Buffer | null = null;
            if (bucketName && sourcePath) {
                // Get original image buffer from S3
                const command = new GetObjectCommand({
                    Bucket: bucketName,
                    Key: sourcePath,
                });
                const s3Object = await s3Client.send(command);
                originalBuffer = await this.streamToBuffer(s3Object.Body as Readable);
                // Save original as WebP
                const webpOriginalBuffer = await sharp(originalBuffer).webp({ lossless: true }).toBuffer();
                const webpOriginalKey = sourcePath.replace(/\.[^/.]+$/, '.webp');
                
                const upload = new Upload({
                    client: s3Client,
                    params: {
                        Bucket: bucketName,
                        Key: webpOriginalKey,
                        Body: webpOriginalBuffer,
                    }
                });
                await upload.done();
            } else {

                // Local: Save original as WebP
                const webpOriginalPath = sourcePath.replace(/\.[^/.]+$/, '.webp');
                await sharp(sourcePath).webp({ lossless: true }).toFile(webpOriginalPath);
            }
            // Create thumbnails
            for (const size of sizeArray) {
                const [w, h] = size.split('x').map(Number);
                if (!w || !h) continue;
                const width = parseInt(w as any);
                const height = parseInt(h as any);

                if (bucketName && sourcePath && originalBuffer) {
                    // Resize to thumbnail
                    const resizedBuffer = await sharp(originalBuffer)
                        .resize({ width, height, fit: sharp.fit.inside, background: { r: 255, g: 255, b: 255, alpha: 1 } })
                        .toBuffer();

                    // Upload resized JPG/PNG
                    const thumbKey = this.getResizedPath(sourcePath, size);
                    const uploadJpg = new Upload({
                        client: s3Client,
                        params: {
                            Bucket: bucketName,
                            Key: thumbKey,
                            Body: resizedBuffer
                        }
                    });
                    await uploadJpg.done();

                    // Upload resized WebP
                    const webpThumbKey = thumbKey.replace(/\.[^/.]+$/, '.webp');
                    const resizedWebpBuffer = await sharp(resizedBuffer).webp({ lossless: true }).toBuffer();
                    const uploadWebp = new Upload({
                        client: s3Client,
                        params: {
                            Bucket: bucketName,
                            Key: webpThumbKey,
                            Body: resizedWebpBuffer
                        }
                    });
                    await uploadWebp.done();

                } else {
                    // Local resized thumbnail
                    const resizedPath = this.getResizedPath(sourcePath, size);
                    const resized = sharp(sourcePath)
                        .resize({ width, height, fit: sharp.fit.inside, background: { r: 255, g: 255, b: 255, alpha: 1 } });
                    let output;
                    switch (extension) {
                        case 'jpeg':
                        case 'jpg':
                            output = resized.jpeg({
                                quality: 90,
                                chromaSubsampling: '4:4:4'
                            });
                            break;
                        case 'webp':
                            output = resized.webp({
                                quality: 90,
                                nearLossless: true,
                                smartSubsample: true
                            });
                            break;
                        case 'png':
                            output = resized.png({
                                compressionLevel: 8,
                                quality: 90,
                                adaptiveFiltering: true
                            });
                            break;
                        case 'avif':
                            output = resized.avif({
                                quality: 85,
                                effort: 4
                            });
                            break;
                        case 'tiff':
                            output = resized.tiff({
                                quality: 90,
                                compression: 'lzw'
                            });
                            break;
                        default:
                            // fallback: preserve original format without applying quality
                            output = resized;
                            break;
                    }
                    await output.toFile(resizedPath);
                    // Save WebP version
                    const resizedWebpPath = resizedPath.replace(/\.[^/.]+$/, '.webp');
                    await sharp(sourcePath)
                        .resize({ width, height, fit: sharp.fit.inside, background: { r: 255, g: 255, b: 255, alpha: 1 } })
                        .webp({ lossless: true })
                        .toFile(resizedWebpPath);
                }
            }
            return true;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    };

    private generateVideoThumbnails = async (sourcePath: string, bucketName?: string | null): Promise<boolean> => {
        try {
            if (!process.env.THUMB_NAIL_OPTIONS) return true;
            const timestamp = Date.now();
            const sizeArray = process.env.THUMB_NAIL_OPTIONS.split(',').filter(Boolean);
            const tempThumbFile = `video-thumb-${timestamp}.jpg`;
            const tempThumbPath = Path.join(tempDir, tempThumbFile);
            let videoSource = sourcePath;
            if (bucketName) {
                const command = new GetObjectCommand({ Bucket: bucketName, Key: sourcePath });
                const s3Object = await s3Client.send(command);
                const buffer = await this.streamToBuffer(s3Object.Body as Readable);
                const tempVideoPath = this.getTempVideoPath(sourcePath);
                await FsPromises.writeFile(tempVideoPath, buffer);
                videoSource = tempVideoPath;
            }

            const extractFrame = (): Promise<string> => {
                return new Promise((resolve, reject) => {
                    ffmpeg(videoSource)
                        .screenshots({
                            timestamps: ['1'],
                            filename: Path.basename(tempThumbPath),
                            folder: Path.dirname(tempThumbPath),
                            size: '1280x?'
                        })
                        .on('end', () => resolve(tempThumbPath))
                        .on('error', reject);
                });
            };
            const thumbPath = await extractFrame();
            const buffer = await FsPromises.readFile(thumbPath);
            const originalThumbName = sourcePath.replace(/\.[^/.]+$/, '.webp');
            const webpBuffer = await sharp(buffer).webp({ lossless: true }).toBuffer();
            if (bucketName) {
                const upload = new Upload({
                    client: s3Client,
                    params: {
                        Bucket: bucketName,
                        Key: originalThumbName,
                        Body: webpBuffer
                    }
                });
                await upload.done();
            } else {
                await Fs.writeFile(originalThumbName, webpBuffer);
            }
            for (const size of sizeArray) {
                const [w, h] = size.split('x').map(Number);
                if (!w || !h) continue;
                const width = parseInt(w as any);
                const height = parseInt(h as any);
                const resizedBuffer = await sharp(buffer).resize({ width, height, fit: sharp.fit.inside }).toBuffer();
                const webpBuffer = await sharp(resizedBuffer).webp({ lossless: true }).toBuffer();
                const thumbName = this.getResizedPath(sourcePath, size).replace(/\.[^/.]+$/, '.webp');
                if (bucketName) {
                    const upload = new Upload({
                        client: s3Client,
                        params: {
                            Bucket: bucketName,
                            Key: thumbName,
                            Body: webpBuffer
                        }
                    });
                    await upload.done();
                } else {

                    const localOutputPath = thumbName;
                    await Fs.writeFile(localOutputPath, webpBuffer);
                }
            }
            return true;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    };

    // File system upload 
    private getResizedPath = (filePath: string, suffix: string): string => {
        try {
            const dir = Path.dirname(filePath);
            const ext = Path.extname(filePath);
            const baseName = Path.basename(filePath, ext);
            const newFileName = `${baseName}_${suffix}${ext}`;
            return Path.posix.join(dir, newFileName);
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // 

    private getFormatPath = (filePath: string, format: string): string => {
        try {
            const dir = Path.dirname(filePath);
            const ext = Path.extname(filePath);
            const baseName = Path.basename(filePath, ext);
            const newFileName = `${baseName}.${format}`;
            return Path.posix.join(dir, newFileName);
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    private buildThumbnailsMetadata = (filePath: string, extension: string): Record<string, unknown> | null => {
        try {
            const normalizedExtension = extension.startsWith(".") ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
            const metadata: {
                originalPath: string;
                originalWebpPath?: string;
                thumbnailPaths: string[];
                webpThumbnailPaths: string[];
                videoResolutionPaths: string[];
            } = {
                originalPath: filePath,
                thumbnailPaths: [],
                webpThumbnailPaths: [],
                videoResolutionPaths: [],
            };

            if (process.env.THUMB_NAIL_OPTIONS) {
                const thumbnailSizes = process.env.THUMB_NAIL_OPTIONS.split(",").map((size) => size.trim()).filter(Boolean);
                for (const size of thumbnailSizes) {
                    const thumbnailPath = this.getResizedPath(filePath, size);
                    metadata.thumbnailPaths.push(thumbnailPath);
                    metadata.webpThumbnailPaths.push(thumbnailPath.replace(/\.[^/.]+$/, ".webp"));
                }
            }

            if (imageFileExtensions.includes(normalizedExtension) || videoExtensions.includes(normalizedExtension)) {
                metadata.originalWebpPath = filePath.replace(/\.[^/.]+$/, ".webp");
            }

            if (videoExtensions.includes(normalizedExtension) && process.env.VIDEO_RESOLUTIONS) {
                const videoResolutionSizes = process.env.VIDEO_RESOLUTIONS.split(",").map((size) => size.trim()).filter(Boolean);
                for (const resolution of videoResolutionSizes) {
                    metadata.videoResolutionPaths.push(this.getResizedPath(filePath, resolution));
                }
            }

            return metadata;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    private buildPublicAttachmentUrl = (uniqueName: string, size: string | null = null, format: string | null = null): string => {
        try {
            const baseUrl = `${process.env.PROTOCOL}://${process.env.API_HOST}/attachment/${uniqueName}`;
            const query: string[] = [];
            if (format) {
                query.push(`format=${encodeURIComponent(format)}`);
            }
            if (size) {
                query.push(`size=${encodeURIComponent(size)}`);
            }
            return query.length ? `${baseUrl}?${query.join("&")}` : baseUrl;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    private extractSizeToken = (pathValue: string): string | null => {
        try {
            const fileName = Path.basename(pathValue);
            const match = fileName.match(/_([^_]+)\.[^/.]+$/);
            return match ? match[1] : null;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    private mapThumbnailsToPublicUrls = (uniqueName: string, thumbnails: any): Record<string, unknown> | null => {
        try {
            if (!thumbnails || typeof thumbnails !== "object") {
                return null;
            }

            const thumbnailPaths = Array.isArray(thumbnails.thumbnailPaths) ? thumbnails.thumbnailPaths as string[] : [];
            const videoResolutionPaths = Array.isArray(thumbnails.videoResolutionPaths) ? thumbnails.videoResolutionPaths as string[] : [];

            const thumbnailSizes = thumbnailPaths
                .map((pathValue) => this.extractSizeToken(pathValue))
                .filter((token): token is string => !!token);

            const videoResolutions = videoResolutionPaths
                .map((pathValue) => this.extractSizeToken(pathValue))
                .filter((token): token is string => !!token);

            const sizes: Record<string, string> = {};
            const sizesWebp: Record<string, string> = {};
            const videoResolutionMap: Record<string, string> = {};

            thumbnailSizes.forEach((size) => {
                sizes[size] = this.buildPublicAttachmentUrl(uniqueName, size, null);
                sizesWebp[size] = this.buildPublicAttachmentUrl(uniqueName, size, "webp");
            });

            videoResolutions.forEach((size) => {
                videoResolutionMap[size] = this.buildPublicAttachmentUrl(uniqueName, size, null);
            });

            return {
                original: this.buildPublicAttachmentUrl(uniqueName),
                originalWebp: this.buildPublicAttachmentUrl(uniqueName, null, "webp"),
                sizes,
                sizesWebp,
                videoResolutions: videoResolutionMap,
            };
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // file handler
    private fileHandler = async (file: RequestQuery, options: fileOptions): Promise<AttachmentInterface> => {
        try {
            const extension = Path.extname(file.hapi.filename);
            const name = uuid.v1() + extension;
            const Resizedname = 'thumb_' + name;
            const destinationPath = `${options.dest}${name}`;
            const destinationPathResized = `${options.dest}${Resizedname}`;
            const fileStream = await Fs.createWriteStream(destinationPath);
            file.pipe(fileStream);
            await finished(fileStream);
            if (imageFileExtensions.includes(extension.toLowerCase())) {
                let imageMeta = await sharp(destinationPath).metadata();
                if (imageMeta.width != undefined && imageMeta.width > 800 || imageMeta.height != undefined && imageMeta.height > 800) {
                    if (extension == '.png') {
                        sharp(destinationPath).resize({ fit: sharp.fit.inside, width: 800, height: 800 }).png({ compressionLevel: 9, quality: 90 }).toFile(destinationPathResized).then(() => {
                            Fs.unlink(destinationPath, (() => {
                                Fs.rename(destinationPathResized, destinationPath, (() => { }));
                            }));
                        });
                    } else {
                        sharp(destinationPath).resize({ fit: sharp.fit.inside, width: 800, height: 800 }).toFile(destinationPathResized).then(() => {
                            Fs.unlink(destinationPath, (() => {
                                Fs.rename(destinationPathResized, destinationPath, (() => { }));
                            }));
                        });
                    }
                }
                // generate required thumbnails
                this.generateThumbnails(destinationPath, extension)

            } else if (videoExtensions.includes(extension.toLowerCase())) {
                const isS3Enabled = process.env.USE_S3_BICKET === 'true';
                const bucketName = isS3Enabled ? process.env.S3_BUCKET_NAME : null
                this.generateVideoThumbnails(destinationPath, bucketName)
                this.generateVideoResolutions(destinationPath, options.dest, process.env.VIDEO_RESOLUTIONS_MAP?.split(','), null)
            }
            const { size } = Fs.statSync(destinationPath);
            const thumbnails = this.buildThumbnailsMetadata(destinationPath, extension);
            const fileDetails: AttachmentInterface = {
                userId: options.userId,
                accountId: options.accountId,
                uniqueName: name,
                fileName: file.hapi.filename,
                extension: extension.replace('.', ''),
                filePath: destinationPath,
                size: size,
                type: Constants.ATTACHMENT.TYPE.FILE_SYSTEM,
                thumbnails,
                status: Constants.ATTACHMENT.STATUS.ACTIVE
            }
            return fileDetails;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    private generateVideoResolutions = async (sourcePath: string, outputDir: string, resolutions = ['1080p', '720p', '480p', '360p'], bucketName: string | null) => {
        try {
            let videoSource = sourcePath;
            const extension = Path.extname(sourcePath);
            if (bucketName) {
                const command = new GetObjectCommand({ Bucket: bucketName, Key: sourcePath });
                const s3Object = await s3Client.send(command);
                const buffer = await this.streamToBuffer(s3Object.Body as Readable);
                const tempVideoPath = this.getTempVideoPath(sourcePath);
                await FsPromises.writeFile(tempVideoPath, buffer);
                videoSource = tempVideoPath;
            }


            const resolutionMap = process.env.VIDEO_RESOLUTIONS_MAP?.split(',');
            const resolution = process.env.VIDEO_RESOLUTIONS?.split(',');
            if (resolution && resolutionMap && resolutionMap.length > 0 && resolution.length == resolutionMap.length) {
                resolutions.forEach((res: string, index: number) => {
                    const size = res;
                    if (!size) { console.warn(`Unsupported resolution: ${res}`); return; }
                    const outputPath = Path.join(outputDir, `${Path.basename(sourcePath, Path.extname(sourcePath))}_${resolution[index]}${extension}`);
                    ffmpeg(sourcePath)
                        .outputOptions('-preset veryfast')
                        .outputOptions('-movflags +faststart')
                        .outputOptions('-pix_fmt yuv420p')
                        .videoCodec('libx264')
                        .audioCodec('aac')
                        .audioBitrate('128k')
                        .size(size)
                        .on('start', (cmd) => {
                            console.log(`[${res}] Starting: ${cmd}`);
                        })
                        .on('error', (err) => {
                            console.log(`[${res}] Error: ${err.message}`);
                        })
                        .on('end', () => {
                            console.log(`[${res}] Done: ${outputPath}`);
                        })
                        .save(outputPath);

                });
            }
            return true;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // Create directory structure
    private createFolderIfNotExists = (createDirectory: boolean): string => {
        try {
            const dt = new Date();
            const folder = dt.getUTCFullYear() + "/" + dt.getUTCMonth() + "/" + dt.getUTCDate() + '/';
            const targetDir = 'resources/attachments/' + folder;
            if (createDirectory)
                Fs.mkdirSync(targetDir, { recursive: true }, 0o777);
            return targetDir;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    uploadFile = async (data: AttachmentUploadServiceInput): Promise<attachmentObjectInteface> => {
        try {
            const isS3Enabled = process.env.USE_S3_BICKET === 'true';
            if (isS3Enabled) {
                return await this.uploadToS3(data);
            }
            return await this.uploadToFileSystem(data);
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // Upload to filesystem
    uploadToFileSystem = async (data: AttachmentUploadServiceInput): Promise<attachmentObjectInteface> => {
        try {
            const filePath = this.createFolderIfNotExists(true);
            const fileOptions: fileOptions = {
                userId: this.userId,
                accountId: data.isGlobal ? null : this.accountId,
                dest: filePath
            }
            let fileDetails = await this.fileHandler(data.file, fileOptions);
            if ((fileDetails && fileDetails.hasOwnProperty('uniqueName')) || (Array.isArray(fileDetails) && fileDetails && fileDetails.length)) {
                fileDetails = Array.isArray(fileDetails) ? fileDetails : fileDetails;
                const saveAttachmentInput: AttachmentSaveDaoInput = { attachmentData: fileDetails };
                let attachment = await this.attachmentDao.saveAttachment(saveAttachmentInput);
                if (attachment) {
                    delete attachment.dataKey;
                    attachment['filePath'] = this.buildPublicAttachmentUrl(attachment.uniqueName);
                    const publicThumbnails = this.mapThumbnailsToPublicUrls(attachment.uniqueName, attachment.thumbnails);
                    if (publicThumbnails) {
                        attachment['thumbnails'] = publicThumbnails;
                    }
                    return attachment;
                }
            }
            throw new AppError(400, 'ERROR_WHILE_SAVING_FILE', {});
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    // S3 upload 
    uploadToS3 = async (data: AttachmentUploadServiceInput): Promise<attachmentObjectInteface> => {
        try {
            const extension = Path.extname(data.file.hapi.filename);
            const uniqueName = uuid.v1() + extension;
            const bucketName = process.env.S3_BUCKET_NAME;
            const fileName = this.createFolderIfNotExists(false);
            const fileContent = Buffer.from(data.file._data, 'binary');
            const destinationPath = process.env.S3_KEY_PREFIX + fileName;
            const s3Path = destinationPath + uniqueName;
            let userId = this.userId;
            let accountId = this.accountId;
            // Uploading files to the bucket
            const upload = new Upload({
                client: s3Client,
                params: {
                    Bucket: bucketName!,
                    Key: s3Path,
                    Body: fileContent
                }
            });
            await upload.done();

            let totalBytes: number = fileContent.length;
            if (totalBytes) {
                const thumbnails = this.buildThumbnailsMetadata(s3Path, extension);
                const fileDetails = {
                    uniqueName: uniqueName,
                    fileName: data.file.hapi.filename,
                    extension: extension.replace('.', ''),
                    filePath: s3Path,
                    size: totalBytes,
                    thumbnails,
                    userId: userId,
                    accountId: data.isGlobal ? null : accountId,
                    status: Constants.ATTACHMENT.STATUS.INACTIVE,
                    type: Constants.ATTACHMENT.TYPE.S3_BUCKET,
                }
                const saveAttachmentInput: AttachmentSaveDaoInput = { attachmentData: fileDetails };
                let attachment = await this.attachmentDao.saveAttachment(saveAttachmentInput);
                if (attachment) {
                    if (imageFileExtensions.includes(extension.toLowerCase())) {
                        this.generateThumbnails(s3Path, extension, bucketName)
                    } else if (videoExtensions.includes(extension.toLowerCase())) {
                        this.generateVideoThumbnails(s3Path, bucketName)
                        this.generateVideoResolutions(s3Path, destinationPath, process.env.VIDEO_RESOLUTIONS_MAP?.split(','), bucketName ? bucketName : null)
                    }
                    delete attachment.dataKey;
                    attachment['filePath'] = this.buildPublicAttachmentUrl(attachment.uniqueName);
                    const publicThumbnails = this.mapThumbnailsToPublicUrls(attachment.uniqueName, attachment.thumbnails);
                    if (publicThumbnails) {
                        attachment['thumbnails'] = publicThumbnails;
                    }
                    return attachment;
                }
            }
            throw new AppError(400, 'ERROR_WHILE_SAVING_FILE', {});
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    processUniqueName = async (uniqueName: string) => {
        let fileUniqueName = uniqueName;
        let fileThumbnailSize: string | null = null;
        let videoResolution: string | null = null;
        // --- Handle thumbnails ---
        const thumbnailSizes = process.env.THUMB_NAIL_OPTIONS ? process.env.THUMB_NAIL_OPTIONS.split(',') : [];
        const lastDotIndex = uniqueName.lastIndexOf('.');
        const nameWithoutExt = uniqueName.substring(0, lastDotIndex);
        const fileExtension = uniqueName.substring(lastDotIndex);
        for (const option of thumbnailSizes) {
            if (nameWithoutExt.endsWith(`_${option}`)) {
                fileThumbnailSize = option;
                const baseName = nameWithoutExt.replace(new RegExp(`_${option}$`), "");
                fileUniqueName = baseName + fileExtension;
                break;
            }
        }
        // --- Handle video resolutions (reverse: strip resolution suffix) ---
        if (process.env.VIDEO_RESOLUTIONS && videoExtensions.includes(fileExtension)) {
            const videoResolutionSizes = process.env.VIDEO_RESOLUTIONS.split(',');
            for (const resolution of videoResolutionSizes) {
                if (nameWithoutExt.endsWith(`_${resolution}`)) {
                    videoResolution = resolution;
                    const baseName = nameWithoutExt.replace(new RegExp(`_${resolution}$`), "");
                    fileUniqueName = baseName + fileExtension;
                    break;
                }
            }
        }
        return { fileUniqueName, fileThumbnailSize, videoResolution };
    }

    getFileByUniqueIdentifier = async (data: AttachmentGetFileByUniqueIdentifierServiceInput): Promise<fileStreamData> => {
        try {
            const { id, uniqueName, size, format = null } = data;
            let originalUniqueName;
            let thumbnailSize;
            let videoResolutionSize
            if (uniqueName) {
                let { fileUniqueName, fileThumbnailSize, videoResolution } = await this.processUniqueName(uniqueName);
                originalUniqueName = fileUniqueName;
                thumbnailSize = fileThumbnailSize;
                videoResolutionSize = videoResolution;
            }
            else if (id) {
                originalUniqueName = undefined;
            }
            const getFileInfoInput: AttachmentGetFileInfoServiceInput = { id, uniqueName: originalUniqueName };
            const attachment = await this.getFileInfo(getFileInfoInput);
            if (!attachment) {
                throw new AppError(404, 'FILE_NOT_FOUND', {});
            }
            // Update filePath based on thumbnail or requested size
            if (uniqueName && thumbnailSize) {
                attachment.filePath = this.getResizedPath(attachment.filePath, thumbnailSize);
            }
            else if (uniqueName && videoResolutionSize) {
                attachment.filePath = this.getResizedPath(attachment.filePath, videoResolutionSize);
            }
            else {
                if (size)
                    attachment.filePath = this.getResizedPath(attachment.filePath, size);
                if (format)
                    attachment.filePath = this.getFormatPath(attachment.filePath, format);
            }
            let streamData: Readable;
            if (attachment.type === Constants.ATTACHMENT.TYPE.FILE_SYSTEM) {
                const stream = Fs.createReadStream(attachment.filePath);
                streamData = new Readable().wrap(stream);
            } else {
                const command = new GetObjectCommand({
                    Bucket: process.env.S3_BUCKET_NAME!,
                    Key: attachment.filePath,
                });
                const s3Object = await s3Client.send(command);
                streamData = s3Object.Body as Readable;
            }

            return { streamData, attachment };
        } catch (err) {
            if (err instanceof AppError) throw err;
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    };

    // get image info
    getFileInfo = async (data: AttachmentGetFileInfoServiceInput) => {
        try {
            const { id, uniqueName } = data;
            let attachment;
            switch (true) {
                case !!id:
                    attachment = await this.attachmentDao.getFileById({ id });
                    break;

                case !!uniqueName:
                    attachment = await this.attachmentDao.getFileByName({ uniqueName });
                    break;

                default:
                    throw new AppError(404, 'FILE_NOT_FOUND', {});
            }
            if (attachment)
                return JSON.parse(JSON.stringify(attachment));
            else {
                throw new AppError(404, 'FILE_NOT_FOUND', {});
            }
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    unlinkFileAndThumbnails = async (filePath: string) => {
        try {
            const dir = Path.dirname(filePath);
            const originalExt = Path.extname(filePath).toLowerCase();
            const base = Path.basename(filePath, originalExt); // filename without extension

            const isImage = ['.jpg', '.jpeg', '.png', '.webp'].includes(originalExt);
            const isVideo = ['.mp4', '.mov', '.avi', '.mkv'].includes(originalExt);

            const thumbnailSizes = process.env.THUMB_NAIL_OPTIONS
                ? process.env.THUMB_NAIL_OPTIONS.split(',').map(s => s.trim())
                : [];

            const videoResolutions = process.env.VIDEO_RESOLUTIONS
                ? process.env.VIDEO_RESOLUTIONS.split(',').map(r => r.trim())
                : [];

            const extensionsToCheck = [originalExt, '.webp'];

            const files = await FsPromises.readdir(dir);

            for (const ext of extensionsToCheck) {
                const baseFile = `${base}${ext}`;
                const fullPath = Path.join(dir, baseFile);

                // Delete the original file
                if (files.includes(baseFile)) {
                    await FsPromises.unlink(fullPath);
                }

                // Delete only expected thumbnails based on environment
                let expectedThumbs: string[] = [];

                if (isImage) {
                    expectedThumbs = thumbnailSizes.map(size => `${base}_${size}${ext}`);
                } else if (isVideo) {
                    let expectedVideoThumbs = videoResolutions.map(res => `${base}_${res}${ext}`);
                    let expectedImageThumbs = thumbnailSizes.map(size => `${base}_${size}${ext}`);
                    expectedThumbs = [...expectedVideoThumbs, ...expectedImageThumbs]
                }

                for (const thumb of expectedThumbs) {
                    if (files.includes(thumb)) {
                        const thumbPath = Path.join(dir, thumb);
                        await FsPromises.unlink(thumbPath);
                    }
                }
            }
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_WHILE_UNLINK_FILE', err);
        }
    };

    unlinkS3FileAndThumbnails = async (filePath: string) => {
        try {
            const bucketName = process.env.S3_BUCKET_NAME;

            if (!bucketName) {
                throw new AppError(500, 'S3_BUCKET_NAME_NOT_CONFIGURED');
            }

            const originalExt = Path.extname(filePath).toLowerCase();
            const base = Path.basename(filePath, originalExt);
            const dir = Path.dirname(filePath);

            const isImage = ['.jpg', '.jpeg', '.png', '.webp'].includes(originalExt);
            const isVideo = ['.mp4', '.mov', '.avi', '.mkv'].includes(originalExt);

            const thumbnailSizes = process.env.THUMB_NAIL_OPTIONS
                ? process.env.THUMB_NAIL_OPTIONS.split(',').map(s => s.trim())
                : [];

            const videoResolutions = process.env.VIDEO_RESOLUTIONS
                ? process.env.VIDEO_RESOLUTIONS.split(',').map(r => r.trim())
                : [];

            const extensionsToCheck = [originalExt, '.webp'];

            const keysToDelete: { Key: string }[] = [];

            // Add original files for all relevant extensions
            for (const ext of extensionsToCheck) {
                const originalKey = Path.posix.join(dir, `${base}${ext}`);
                keysToDelete.push({ Key: originalKey });

                // Thumbnails
                let expectedThumbs: string[] = [];

                if (isImage) {
                    expectedThumbs = thumbnailSizes.map(size => Path.posix.join(dir, `${base}_${size}${ext}`));
                } else if (isVideo) {
                    const resThumbs = videoResolutions.map(res => Path.posix.join(dir, `${base}_${res}${ext}`));
                    const imageThumbs = thumbnailSizes.map(size => Path.posix.join(dir, `${base}_${size}${ext}`));
                    expectedThumbs = [...resThumbs, ...imageThumbs];
                }

                for (const thumbKey of expectedThumbs) {
                    keysToDelete.push({ Key: thumbKey });
                }
            }

            // Remove duplicates just in case
            const uniqueKeys = Array.from(new Set(keysToDelete.map(obj => obj.Key))).map(Key => ({ Key }));

            if (uniqueKeys.length > 0) {
                const command = new DeleteObjectsCommand({
                    Bucket: bucketName,
                    Delete: {
                        Objects: uniqueKeys as { Key: string }[],
                        Quiet: true,
                    },
                });
                await s3Client.send(command);
            }

        } catch (err) {
            if (err instanceof AppError) throw err;
            throw new AppError(500, 'SOMETHING_WENT_WRONG_WHILE_UNLINK_S3_FILE', err);
        }
    };
    deleteFile = async (attachment: AttachmentInterface) => {
        try {
            if (!attachment || !attachment.id || !attachment.filePath) {
                throw new AppError(400, 'INVALID_ATTACHMENT_DATA');
            }

            if (attachment.type === 1) {
                // Local file system
                this.unlinkFileAndThumbnails(attachment.filePath);
            } else {
                // S3 storage
                this.unlinkS3FileAndThumbnails(attachment.filePath);
            }

            // Delete the record from the database
            const deleteFileInput: AttachmentDeleteDaoInput = { id: attachment.id };
            await this.attachmentDao.deleteFile(deleteFileInput);

        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    };

    deleteAttachment = async (data: AttachmentDeleteServiceInput): Promise<void> => {
        try {
            const getFileInfoInput: AttachmentGetFileInfoServiceInput = { id: data.id, uniqueName: data.uniqueName };
            let fileToDelete = await this.getFileInfo(getFileInfoInput);
            if (!fileToDelete) {
                throw new AppError(404, 'FILE_NOT_FOUND');
            }
            if (typeof data.authUserData !== 'boolean') {
                const permissions = data.authUserData.permissions;
                let isAdmin = false;
                if (permissions && permissions.includes('admin')) {
                    isAdmin = true;
                }
                const isOwner = data.authUserData?.userId === fileToDelete?.author?.id;
                if (!isAdmin && !isOwner) {
                    throw new AppError(401, 'NOT_AUTHORIZE_TO_TAKE_THIS_ACTION');
                }
            }
            await this.deleteFile(fileToDelete);
            return;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    getAttachmentsForEmail = async (attachments: number[]) => {
        try {
            let mailAttachments: mailAttachment[] = [];
            if (attachments && attachments.length) {
                const getAttachmentsInput: AttachmentGetAllDaoInput = { ids: attachments, uniqueNames: null, fullObject: false };
                let attachmentData = await this.attachmentDao.getAttachments(getAttachmentsInput);
                for (let attachment of attachmentData) {
                    let newAttachment: mailAttachment = { filename: undefined, content: undefined, path: undefined };
                    if (attachment.type == 2) {
                        newAttachment.filename = attachment.fileName;
                        const command = new GetObjectCommand({
                            Bucket: process.env.S3_BUCKET_NAME!,
                            Key: attachment.filePath,
                        });
                        const data = await s3Client.send(command);
                        const buffer = await this.streamToBuffer(data.Body as Readable);
                        newAttachment.content = buffer;

                    } else {
                        newAttachment.filename = attachment.fileName,
                            newAttachment.path = "./" + attachment.filePath
                    }
                    mailAttachments.push(newAttachment)
                }
            }
            return mailAttachments;
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }

    getAllFileInfo = async (data: AttachmentGetAllFileInfoServiceInput) => {
        try {
            const { listRequest } = data;
            const { perPage, page } = listRequest;
            let attachment: attachmentObjectInteface[];
            const getAllAttachmentsInput: AttachmentGetAllAttachmentsDaoInput = { listRequest, fullObject: true };
            const result = await this.attachmentDao.getAllAttachments(getAllAttachmentsInput);
            attachment = result.rows;
            let totalPages = await Common.getTotalPages(result.count, listRequest.perPage);
            if (attachment) {
                attachment.forEach(element => {
                    element.filePath = this.buildPublicAttachmentUrl(element.uniqueName);
                    const publicThumbnails = this.mapThumbnailsToPublicUrls(element.uniqueName, element.thumbnails);
                    if (publicThumbnails) {
                        element.thumbnails = publicThumbnails as unknown as string[];
                        const videoResolutionMap = (publicThumbnails.videoResolutions as Record<string, string>) || {};
                        element.videoResolution = Object.values(videoResolutionMap);
                    } else {
                        element.thumbnails = [];
                        element.videoResolution = [];
                    }
                    delete element.dataKey;
                });
                return {
                    data: attachment,
                    page: page,
                    perPage: perPage,
                    totalRecords: result.count,
                    totalPages: totalPages
                } as unknown as AttachmentListRequestObject;
            }
            else {
                throw new AppError(404, 'FILE_NOT_FOUND', {});
            }
        } catch (err) {
            if (err instanceof AppError) { throw err; }
            throw new AppError(500, 'SOMETHING_WENT_WRONG_IN_SERVICE', err);
        }
    }
    uploadImageFromUrl = async (imageUrl: string) => {
        try {
          const response = await Axios.get(imageUrl, { responseType: "stream" });
          const fileName = Path.basename(new URL(imageUrl).pathname) || "file";
          const formData = new FormData();
          formData.append("file", response.data, fileName);
          formData.append("isGlobal", "true");
          const baseUrl = `${process.env.PROTOCOL}://${process.env.API_HOST}`;
          const uploadResponse = await Axios.post(`${baseUrl}/attachment`, formData, {
            headers: {
              ...formData.getHeaders(),
              "PREFERRED_LANGUAGE": "en",
              "PREFERRED_TIMEZONE": "UTC",
              "ACCOUNT_KEY": "default-key",
              "CONNECTION_PREFERENCE": "keep-alive",
            },
            maxBodyLength: Infinity,
          });
          return uploadResponse.data.responseData;
        } catch (err) {
          if (err instanceof AppError) throw err;
          throw new AppError(500, "SOMETHING_WENT_WRONG_IN_SERVICE", err);
        }
    };
}
