import { ConfService } from "@libs/library-app/conf/conf.service";
import { EntityCRUDTypeDefinition, EtcCRUDTypeDefinition, FileRelocationDtoCRUDTypeDefinition, FileRelocationInputArtefactCRUDTypeDefinition, FileRelocationOutputArtefactCRUDTypeDefinition, SnapshotListArtefactCRUDTypeDefinition } from "../crud.type";
import { RootCrudEngine } from "./root.engine";
import { LogService } from "@libs/library-app/log/log.service";
import { DataValidationPipe } from "@libs/library-app/data-validation/data.validation.pipe";
import { LibraryAppService } from "@libs/library-app/library.app.service";
import { SelfGraphqlMicroserviceService } from "@libs/dynamic-app";
import { BadRequestException, Type } from "@nestjs/common";
import { DataSource, getMetadataArgsStorage, In, Repository } from "typeorm";
import { GraphQLResolveInfo } from "graphql";
import { getEntityUploadFieldMetadata, UploadFieldMeta } from "@libs/library-app/upload/upload.decorator";
import { FileRelocationTypeEnum } from "../crud.enum";
import { UploadCrudEngine } from "./upload.engine";
import path from "path";
import fs from 'fs-extra';
import { String } from "lodash";

export class FileRelocationCrudEngine
<
    // entity (1)
    ENTITY_TYPE extends EntityCRUDTypeDefinition,

    // file_relocation (3)
    FILE_RELOCATION_DTO_TYPE extends FileRelocationDtoCRUDTypeDefinition,
    FILE_RELOCATION_INPUT_TYPE extends FileRelocationInputArtefactCRUDTypeDefinition,
    FILE_RELOCATION_OUTPUT_TYPE extends FileRelocationOutputArtefactCRUDTypeDefinition
>extends RootCrudEngine
<
ENTITY_TYPE
>
{
constructor(
    // data source
    protected readonly dataSource: DataSource,
    protected readonly repository: Repository<ENTITY_TYPE>,

    // dependecy services
    protected readonly confService: ConfService,
    protected readonly logService: LogService,
    protected readonly validationPipe: DataValidationPipe,
    protected readonly libraryAppService: LibraryAppService,
    protected readonly selfGraphqlMicroserviceService: SelfGraphqlMicroserviceService,

    // entity (1)
    protected readonly ENTITY: Type<ENTITY_TYPE> & EntityCRUDTypeDefinition,

    //file_relocation (3)
    protected readonly FILE_RELOCATION_DTO: Type<FILE_RELOCATION_DTO_TYPE> & FileRelocationDtoCRUDTypeDefinition,
    protected readonly FILE_RELOCATION_INPUT_DTO: Type<FILE_RELOCATION_INPUT_TYPE> & FileRelocationInputArtefactCRUDTypeDefinition,
    protected readonly FILE_RELOCATION_OUTPUT_DTO: Type<FILE_RELOCATION_OUTPUT_TYPE> & FileRelocationOutputArtefactCRUDTypeDefinition
)
{
    super(
        // data source
        dataSource,
        repository,

        // dependecy services
        confService,
        logService,
        validationPipe,
        libraryAppService,
        selfGraphqlMicroserviceService,

        // entity (1)
        ENTITY
    );
}
/**
 * ████████████████████████████████████████████████████████████████████████████████████
 * ████ RESOLVER METHODES █████████████████████████████████████████████████████████████
 * ████████████████████████████████████████████████████████████████████████████████████
**/
public async fileRelocation(input: FILE_RELOCATION_INPUT_TYPE | FILE_RELOCATION_INPUT_TYPE[], selection: FILE_RELOCATION_OUTPUT_TYPE, info: GraphQLResolveInfo, ctx: any, etc?: EtcCRUDTypeDefinition): Promise<FILE_RELOCATION_OUTPUT_TYPE[]> {
    try{
        return await this._fileRelocation(input, selection, info, ctx);
    }catch (err: any) {
        this.logService.error(err, err.message);
        throw new BadRequestException(`${this.FILE_RELOCATION_DTO?.metaname}: ${this.logService.redactSensitive(err.message)}`);
    }
}

/**
 * ████████████████████████████████████████████████████████████████████████████████████
 * ████ SERVICE METHODES ██████████████████████████████████████████████████████████████
 * ████████████████████████████████████████████████████████████████████████████████████
**/


public async _fileRelocation(input: FILE_RELOCATION_INPUT_TYPE | FILE_RELOCATION_INPUT_TYPE[], selection: FILE_RELOCATION_OUTPUT_TYPE, info: GraphQLResolveInfo, ctx: any, etc?: EtcCRUDTypeDefinition): Promise<FILE_RELOCATION_OUTPUT_TYPE[]> {
    try{
        // if single record passed convert to array
        if(!Array.isArray(input)){
            input = [input];
        }
        
        // set the array for output
        const respArr: FILE_RELOCATION_OUTPUT_TYPE[] = [];

        // set the flag if multiple recrods are processed
        let mutiRelocate:boolean = false;

        // get primary key field of entity
        const pk_field = await this._getEntityPrimaryKeyFieldName(this.ENTITY) as keyof ENTITY_TYPE as string;
    
        // loop through all input
        for(let i=0; i < input.length; i++){
            const inputItem = input[i];

            const resp = new this.FILE_RELOCATION_OUTPUT_DTO();
            const snapshot: SnapshotListDto = new SnapshotListDto();
            resp.affected = 0;
            resp.file_field = inputItem.file_field;
            resp.id = inputItem.destination_id;
            resp.ref_id = inputItem.destination_ref_id;

            // get upload field of entity
            const entityUploadFieldsMeta = await getEntityUploadFieldMetadata(this.ENTITY.prototype);
                
            // get the current upload requested field meta
            const curUploadFieldMeta = entityUploadFieldsMeta.find(item => item.propertyKey === inputItem.file_field);
            
            // make sure filed has upload feature enabled in entity
            if(curUploadFieldMeta && curUploadFieldMeta.data){

                // get entity field meta for upload related settings
                const fieldMeta = curUploadFieldMeta as UploadFieldMeta;
                
                // make sure source and destination both records are availabe in database
                if(fieldMeta.data.req_max_count == 1 && inputItem.source_id && inputItem.destination_id && (inputItem.source_ref_id == null || inputItem.source_ref_id == undefined)  && (inputItem.destination_ref_id == null || inputItem.destination_ref_id == undefined)){
                    // PROCESS SINGLE FILE
                    
                    // USE CASE
                    // source_id = 1
                    // source_ref_id = N/A
                    // destination_id = 2
                    // destination_ref_id = N/A

                    // get the record from the database
                    const findWhere: any = {};
                    findWhere[pk_field] = In([inputItem.source_id, inputItem.destination_id]);
                    const currRecordArr =  await this.repository.find({ where: findWhere });
                    
                    // Create a Map with the primary key as the key
                    //new Map(currRecordArr.map(record => [record.id, record]));
                    const recordMap = new Map(currRecordArr.map(record => [record.id, record]));
                    const sourceArr = recordMap.get(Number(inputItem.source_id));
                    const destinationArr = recordMap.get(Number(inputItem.destination_id));

                    if(inputItem.relocation_type && currRecordArr.length == 2 && sourceArr && destinationArr){

                        // get destination path and source path 
                        const destinationPath  = await this._uploadFileDestinationPath(fieldMeta, inputItem.destination_id, inputItem.file_field, this.ENTITY.uploaddir);
                        const sourcePath = await this._uploadFileDestinationPath(fieldMeta, inputItem.source_id, inputItem.file_field, this.ENTITY.uploaddir);
                        const sourceFileName = sourceArr[fieldMeta.propertyKey];
                        const destinationFileName = destinationArr[fieldMeta.propertyKey] 
                        
                        try{
                            const relocation = await this._relocateFile(inputItem.relocation_type, sourceFileName, sourcePath, destinationPath);
                            
                            if(relocation == FileRelocationTypeEnum.MOVE){
                                
                                // PROCESS DESTINATION
                                // update file name as new file in destination
                                await this._updateRecordFileName(pk_field, inputItem.destination_id, inputItem.file_field, sourceFileName);
                                
                                // PROCESS SOURCE
                                // update source file as null in database
                                await this._updateRecordFileName(pk_field, inputItem.source_id, inputItem.file_field, null);
                                
                                // remove destination file
                                await fs.remove(path.join(this.confService.cwd, destinationPath, destinationFileName));
                                await fs.remove(path.join(this.confService.cwd, destinationPath, this._getThumbFilename(destinationFileName)))
                            } else if(relocation == FileRelocationTypeEnum.DUPLICATE) {

                                //TODO: update destination file field name as new file in database
                                await this._updateRecordFileName(pk_field, inputItem.destination_id, inputItem.file_field, sourceFileName);

                                // TODO: remove destination file from destination path
                                await fs.remove(path.join(this.confService.cwd, destinationPath, destinationFileName));
                                await fs.remove(path.join(this.confService.cwd, destinationPath, this._getThumbFilename(destinationFileName)))
                            }
                            // get new destination record
                            const nwhere: any = {};
                            nwhere[pk_field] = inputItem.destination_id;
                            const newDestRecordArr = await this.repository.findOne({where: nwhere});
                            
                            resp.access_url = await this._getUploadFileAccessUrl(fieldMeta, inputItem.file_field, newDestRecordArr, this.ENTITY.uploaddir);
                            resp.affected = 1 ;
                            resp.relocation_type = relocation;
                            
                            snapshot.success.push(`File relocated successfully.`);
                        } catch (err: any) {
                            resp.relocation_type = inputItem.relocation_type;
                            snapshot.error.push(`Error relocating file ${sourceFileName} from ${sourcePath} to ${destinationPath}. ${err.message}`);
                        }
                    }
                    else{
                        snapshot.error.push(`Source id ${inputItem.source_id} or ${inputItem.destination_id} not found. Please enter proper source id and destination id to complete the file relocation process`);
                    }
                }
                else if(fieldMeta.data.req_max_count > 1 && inputItem.source_id && inputItem.source_ref_id && inputItem.destination_ref_id){
                    // PROCESS MULTIPLE FILE BUT ONLY ONE AS PER REQUEST

                    //TODO:Function MultiRecordSingleRelocation

                    if(inputItem.destination_id){

                        // USE CASE
                        // source_id = 1
                        // source_ref_id = 10
                        // destination_id = 2
                        // destination_ref_id = 20

                        // get the source record from the database
                        const sourceFindWhere: any = {};
                        sourceFindWhere[pk_field] = inputItem.source_id;
                        sourceFindWhere[fieldMeta.data.ref_id_field] = inputItem.source_ref_id;
                        const sourceRecordArr =  await this.repository.findOne({ where: sourceFindWhere });

                        // get the destination record from the database
                        const destFindWhere: any = {};
                        destFindWhere[pk_field] = inputItem.destination_id;
                        destFindWhere[fieldMeta.data.ref_id_field] = inputItem.destination_ref_id;
                        const destRecordArr =  await this.repository.findOne({ where: destFindWhere });

                        if(sourceRecordArr != null && destRecordArr != null && inputItem.relocation_type){

                            // get destination path and source path 
                            const destinationPath  = await this._uploadFileDestinationPath(fieldMeta, inputItem.destination_ref_id, inputItem.file_field, this.ENTITY.uploaddir);
                            const sourcePath = await this._uploadFileDestinationPath(fieldMeta, inputItem.source_ref_id, inputItem.file_field, this.ENTITY.uploaddir);
                            const sourceFileName = sourceRecordArr[fieldMeta.propertyKey];
                            const destinationFileName = destRecordArr[fieldMeta.propertyKey];

                            try{
                                const relocation = await this._relocateFile(inputItem.relocation_type, sourceFileName, sourcePath, destinationPath);
                                
                                if(relocation == FileRelocationTypeEnum.MOVE){
                                    
                                    // TODO: update file field in destination id
                                    await this._updateRecordFileName(pk_field, inputItem.destination_id, inputItem.file_field, sourceFileName);
                                    
                                    // TODO: remove current destination file from the folder.
                                    await fs.remove(path.join(this.confService.cwd, destinationPath, destinationFileName))
                                    await fs.remove(path.join(this.confService.cwd, destinationPath, this._getThumbFilename(destinationFileName)))
                                    
                                    // ToDo: delete source record
                                    await this._deleteRecord(inputItem.source_id);
                                } else if(relocation == FileRelocationTypeEnum.DUPLICATE) {
                                    
                                    // TODO: update file field in destination id
                                    await this._updateRecordFileName(pk_field, inputItem.destination_id, inputItem.file_field, sourceFileName);

                                    // TODO: remove current destination file from the folder.
                                    await fs.remove(path.join(this.confService.cwd, destinationPath, destinationFileName));
                                    await fs.remove(path.join(this.confService.cwd, destinationPath, this._getThumbFilename(destinationFileName)))
                                }
                                // get new destination record
                                const nwhere:any = {};
                                nwhere[pk_field] = inputItem.destination_id;
                                const newDestRecordArr = await this.repository.findOne({where: nwhere});

                                resp.access_url = await this._getUploadFileAccessUrl(fieldMeta, inputItem.file_field, newDestRecordArr, this.ENTITY.uploaddir);
                                resp.affected = 1;
                                resp.relocation_type = relocation;
                                
                                snapshot.success.push(`File relocated successfully.`);

                            } catch (err: any) {
    
                                resp.relocation_type = inputItem.relocation_type;
                                snapshot.error.push(`Error relocating file ${sourceFileName} from ${sourcePath} to ${destinationPath}. ${err.message}`);
                            }
                        }else{
                            snapshot.error.push(`Provided source and destination record not found. Please provide proper details to complete the file relocation process`); 
                        }
                    } else {
                        // PROCESS FOR ONE FILE TO CREATE AS NEW RECORD 

                        // USE CASE
                        // source_id = 1
                        // source_ref_id = 10
                        // destination_id = N/A
                        // destination_ref_id = 20
                        
                        // get the record from the database
                        const findWhere: any = {};
                        findWhere[pk_field] = inputItem.source_id;
                        findWhere[fieldMeta.data.ref_id_field] = inputItem.source_ref_id;

                        const currRecord =  await this.repository.findOne({ where: findWhere });

                        if(currRecord != null && inputItem.relocation_type){
                            
                            // get destination path and source path 
                            const destinationPath  = await this._uploadFileDestinationPath(fieldMeta, inputItem.destination_ref_id, inputItem.file_field, this.ENTITY.uploaddir);
                            const sourcePath = await this._uploadFileDestinationPath(fieldMeta, inputItem.source_ref_id, inputItem.file_field, this.ENTITY.uploaddir);
                            const sourceFileName = currRecord[fieldMeta.propertyKey];
                            

                            try{
                                const relocation = await this._relocateFile(inputItem.relocation_type, sourceFileName, sourcePath, destinationPath);
                                let newRecordId: any;
                                
                                if(relocation === FileRelocationTypeEnum.MOVE){

                                    //TODO: update ref id field in database
                                    const affected = await this._updateRecordRefIdById(pk_field, inputItem.source_id, fieldMeta.data.ref_id_field, inputItem.destination_ref_id);
                                    newRecordId = inputItem.source_id;
                                }else if(relocation == FileRelocationTypeEnum.DUPLICATE) {
                                    // TODO: create new record based on ref id and file field
                                    // create record in database
                                    newRecordId = await this._createRecord(fieldMeta.data.ref_id_field, inputItem.destination_ref_id, inputItem.file_field, sourceFileName);
                                }
                                
                                if(newRecordId){
                                    // get new insterted record
                                    const nWhere: any = {};
                                    nWhere[pk_field] = newRecordId;
                                    const newRecord = await this.repository.findOne({ where: nWhere});
                                    resp.access_url = await this._getUploadFileAccessUrl(fieldMeta, inputItem.file_field, newRecord, this.ENTITY.uploaddir);
                                }
                                resp.id = newRecordId;
                                resp.affected = 1;
                                resp.relocation_type = relocation;
                                
                                snapshot.success.push(`File relocated successfully.`);

                            } catch (err: any) {
    
                                resp.relocation_type = inputItem.relocation_type;
                                snapshot.error.push(`Error relocating file ${sourceFileName} from ${sourcePath} to ${destinationPath}. ${err.message}`);
                            }
                        }
                    }
                    
                }
                else if(inputItem.source_ref_id && (inputItem.destination_ref_id || inputItem.destination_id)){

                    // USE CASE
                    // source_id = N/A
                    // source_ref_id = 10
                    // destination_id = N/A
                    // destination_ref_id = 20

                    // PROCESS FOR MULTIPLE FILE AS PER REF ID
                    // if destination ref id is not provided then destination id will be used
                    if(!inputItem.destination_ref_id){
                           
                        // USE CASE
                        // source_id = N/A
                        // source_ref_id = 9
                        // destination_id = 10
                        // destination_ref_id = N/A
                       
                        // considering that id is passed
                        const nWhere: any = {};
                        nWhere[pk_field] = inputItem.destination_id;
                        const destRecordArr = await this.repository.findOne({where: nWhere});

                        if(destRecordArr)
                            inputItem.destination_ref_id = String(destRecordArr.destination_ref_id);
                        else
                            snapshot.error.push(`source_id and destination_id OR source_ref_id and destination_ref_id is required. Please provide valid combination input to process file relocation`);
                    }

                    if(inputItem.destination_ref_id){

                        // get the record from the database
                        const findWhere: any = {};
                        findWhere[fieldMeta.data.ref_id_field] = inputItem.source_ref_id;

                        let currRecordArr =  await this.repository.find({ where: findWhere });

                        if(currRecordArr.length > 0 && inputItem.relocation_type){
                            
                            mutiRelocate = true;
                            let relocation: FileRelocationTypeEnum | undefined;
                            
                            // as need to process multiple records loop through all
                            for(let i=0; i < currRecordArr.length; i++){

                                const multiResp = new this.FILE_RELOCATION_OUTPUT_DTO();
                                // get destination path and source path 
                                const destinationPath  = await this._uploadFileDestinationPath(fieldMeta, inputItem.destination_ref_id, inputItem.file_field, this.ENTITY.uploaddir);
                                const sourcePath = await this._uploadFileDestinationPath(fieldMeta, inputItem.source_ref_id, inputItem.file_field, this.ENTITY.uploaddir);
                                const sourceFileName = currRecordArr[i][fieldMeta.propertyKey];
                                
                                try{
                                    relocation = await this._relocateFile(inputItem.relocation_type, sourceFileName, sourcePath, destinationPath);
                                    let newRecordId: any;

                                    if(relocation === FileRelocationTypeEnum.MOVE){
                                        // update source ref id
                                        await this._updateRecordRefIdById(pk_field, currRecordArr[i][pk_field],fieldMeta.data.ref_id_field, inputItem.destination_ref_id);
                                        // set new record id as current source id becuase data is only updated
                                        newRecordId = currRecordArr[i][pk_field];
                                    }else if(relocation == FileRelocationTypeEnum.DUPLICATE){
                                        // TODO: create new record based on ref id and file field 
                                        newRecordId = await this._createRecord(fieldMeta.data.ref_id_field, inputItem.destination_ref_id, inputItem.file_field, sourceFileName);
                                    }
                                    
                                    // get new insterted record
                                    const nWhere: any = {};
                                    nWhere[pk_field] = newRecordId;
                                    const newRecord = await this.repository.findOne({ where: nWhere });

                                    multiResp.access_url = await this._getUploadFileAccessUrl(fieldMeta, inputItem.file_field, newRecord, this.ENTITY.uploaddir);
                                    multiResp.file_field = inputItem.file_field;
                                    multiResp.id = newRecordId;
                                    multiResp.ref_id = inputItem.destination_ref_id;
                                    multiResp.affected = 1;
                                    multiResp.relocation_type = relocation;
                                    
                                    snapshot.success.push(`File relocated successfully.`);
                                    multiResp.snapshot = snapshot

                                } catch (err: any) {
        
                                    multiResp.relocation_type = inputItem.relocation_type;
                                    snapshot.error.push(`Error relocating file ${sourceFileName} from ${sourcePath} to ${destinationPath}. ${err.message}`);
                                }
                                multiResp.snapshot = snapshot;
                                respArr.push(multiResp);
                            }
                        }
                        else{
                            snapshot.error.push(`ref_id ${inputItem.source_ref_id} not found. Please enter proper ref id to complete the file relocation process`);
                        }
                    }
                    else{
                        snapshot.error.push(`source_id and destination_id OR source_ref_id and destination_ref_id is required. Please provide valid combination input to process file relocation`);
                    }
                }
                else{
                    // USE CASE
                    // source_id = 1
                    // source_ref_id = N/A
                    // destination_id = N/A
                    // destination_ref_id = N/A

                    // USE CASE
                    // source_id = N/A
                    // source_ref_id = N/A
                    // destination_id = N/A
                    // destination_ref_id = N/A

                    // USE CASE
                    // source_id = N/A
                    // source_ref_id = N/A
                    // destination_id = N/A
                    // destination_ref_id = 20

                    // USE CASE
                    // source_id = N/A
                    // source_ref_id = N/A
                    // destination_id = 2
                    // destination_ref_id = 20

                    // USE CASE
                    // source_id = 1
                    // source_ref_id = 10
                    // destination_id = N/A
                    // destination_ref_id = N/A

                    snapshot.error.push(`source_id and destination_id OR source_ref_id and destination_ref_id is required. Please provide valid combination input to process file relocation`);
                }
            }
            else{
                snapshot.error.push(`There is no upload feature available as per file_field, so you can not perform file relocation`);
            }

            if(mutiRelocate == false){
                resp.snapshot = snapshot;
                respArr.push(resp);  
            }
        }
        return respArr;
    }
    catch(err: any){
        this.logService.error(err, `Update failed: ${err.message}`);
        throw new BadRequestException(err, err.message);
    }
}
private async _createRecord(refField: string, refId: string, fileField: string, fileName: string): Promise<any> {
    try{
        const req: any = {};
        req[refField] = refId;
        req[fileField] = fileName;
  
        const create = this.repository.create(req);
        const insert = await this.repository.insert(create);

        if(insert.identifiers.length === 1) {
            return insert.identifiers[0].id;
        }
    }
    catch(err: any){
        throw new BadRequestException(err, err.message);
    }
}
private async _updateRecordRefIdById(pkField: string, pkValue: string, refField: string, refId: string): Promise<any> {
    try{
        const uSets: any = {};
        const uWhere: any = {};
        
        uSets[refField] = refId;
        uWhere[pkField] = pkValue;

        getMetadataArgsStorage().columns.find(column => 
            column.propertyName === refField &&
            column.target === this.ENTITY
        )!.options.update = true;
        
        const affectedRow = await this.repository.update(uWhere, uSets);

        getMetadataArgsStorage().columns.find(column => 
            column.propertyName === refField &&
            column.target === this.ENTITY
        )!.options.update = false;

        return affectedRow.affected as number;

    } catch(err: any){
        throw new BadRequestException(err, err.message);
    }
}
private async _updateRecordFileName(pkField: string, pkValue: string, fileField: string, fileName: string | null): Promise<any> {
    try{
        const uSets: any = {};
        const uWhere: any = {};
        
        uSets[fileField] = fileName;
        uWhere[pkField] = pkValue;
        
        const affectedRow = await this.repository.update(uWhere, uSets);

        return affectedRow.affected as number;

    } catch(err: any){
        throw new BadRequestException(err, err.message);
    }
}
private async _deleteRecord(pk_value: string | number): Promise<any>{
    try{
        const resp = await this.repository.delete(pk_value as number);
        return resp;
    } catch(err: any){
        throw new BadRequestException(err, err.message);
    }
}
private async _SingleRecordRelocation(){

}
private async MultiRecordSingleRelocation(){

}

}
