import Title from '#models/title';
import Description from '#models/description';
import License from '#models/license';
import Person from '#models/person';
import DatasetReference from '#models/dataset_reference';
import DatasetIdentifier from '#models/dataset_identifier';
import Subject from '#models/subject';
import File from '#models/file';
import Coverage from '#models/coverage';
import Collection from '#models/collection';
import { BaseModel as LucidBaseModel } from '@adonisjs/lucid/orm';
import Field from '#app/Library/Field';
import { DateTime } from 'luxon';

// @StaticImplements<LucidModel>()
// class LucidDatasetModel extends BaseModel{
//     @belongsTo(() => Dataset, {
//         foreignKey: 'dataset_id',
//     })
//     dataset: BelongsTo<typeof Dataset>;
// }

export type DatasetRelatedModel =
    | typeof Title
    | typeof Description
    | typeof Coverage
    | typeof DatasetIdentifier
    | typeof DatasetReference
    | typeof Description
    | typeof DatasetIdentifier
    | typeof File;

export default abstract class DatasetExtension extends LucidBaseModel {
    public abstract id: number;
    public externalFields: Record<string, any> = this.getExternalFields();
    // which fields shoud#t be published
    protected internalFields: Record<string, any> = {};
    protected fields: Record<string, any> = {};
    // [key: string]: any;

    private getExternalFields(): Record<string, any> {
        // External fields definition
        return {
            TitleMain: {
                model: Title,
                options: { type: ['Main'] },
                fetch: 'eager',
            },
            TitleAdditional: {
                model: Title,
                options: { type: ['Alternative', 'Sub', 'Translated', 'Other'] },
                fetch: 'eager',
            },
            TitleAbstract: {
                model: Description,
                options: { type: ['Abstract', 'Translated'] },
                fetch: 'eager',
            },
            TitleAbstractAdditional: {
                model: Description,
                options: { type: ['Methods', 'Technical_info', 'Series_information', 'Other'] },
                fetch: 'eager',
            },
            Licence: {
                model: License,
                through: 'link_documents_licences',
                relation: 'licenses',
                fetch: 'eager',
            },
            PersonAuthor: {
                model: Person,
                through: 'link_documents_persons',
                pivot: { role: 'author', sort_order: 'sort_order', allow_email_contact: 'allow_email_contact' },
                relation: 'persons',
                fetch: 'eager',
            },
            PersonContributor: {
                model: Person,
                through: 'link_documents_persons',
                pivot: {
                    role: 'contributor',
                    contributor_type: 'contributor_type',
                    sort_order: 'sort_order',
                    allow_email_contact: 'allow_email_contact',
                },
                relation: 'contributors',
                fetch: 'eager',
            },
            Reference: {
                model: DatasetReference,
                relation: 'references',
                fetch: 'eager',
            },
            Identifier: {
                model: DatasetIdentifier,
                relation: 'identifier',
                fetch: 'eager',
            },
            Subject: {
                model: Subject,
                through: 'link_dataset_subjects',
                relation: 'subjects',
                fetch: 'eager',
            },
            File: {
                model: File,
                relation: 'files',
                fetch: 'eager',
            },
            Coverage: {
                model: Coverage,
                relation: 'coverage',
                fetch: 'eager',
            },
            Collection: {
                model: Collection,
                through: 'link_documents_collections',
                relation: 'collections',
                fetch: 'eager',
                //   'include': { 'model': CollectionRole, 'relation': 'collectionrole' }
            },
        };
    }

    public initFields(): void {
        // Initialize internal fields
        let fields = new Array<string>(
            'Id',
            'PublisherName',
            'PublishId',
            'ContributingCorporation',
            'CreatingCorporation',
            'Language',
            'PublishedDate',
            // 'PublishedYear',
            'PublisherName',
            // 'PublisherPlace',
            'PublicationState',
            'EmbargoDate',
            'CreatedAt',
            'ServerDateModified',
            'ServerDatePublished',
            'ServerDateDeleted',
            'ServerState',
            'Type',
            'BelongsToBibliography',
        );
        fields.forEach((fieldname) => {
            let field = new Field(fieldname);
            this.addField(field);
        });
        // Initialize external fields
        const fieldNames = Object.keys(this.externalFields);
        for (const fieldName of fieldNames) {
            // const field = this.externalFields[fieldName];
            let field = new Field(fieldName);
            field.setMultiplicity('*');
            this.addField(field);
        }

        //      // Initialize available date fields and set up date validator
        //     // if the particular field is present
        let dateFields = new Array<string>('EmbargoDate', 'CreatedAt', 'ServerDateModified', 'ServerDatePublished', 'ServerDateDeleted');
        dateFields.forEach((fieldname) => {
            let dateField = this.getField(fieldname);
            dateField instanceof Field && dateField.setValueModelClass(DateTime.now());
        });
    }

    public async describe(): Promise<Array<string>> {
        let length: number = Object.keys(this.fields).length;
        if (length == 0) {
            await this.fetchValues();
        }
        // Get an array of all field names in the 'fields' object
        const allFields = Object.keys(this.fields);
        // Get an array of all field names in the 'internalFields' array
        const internalFields = Object.keys(this.internalFields);
        // Use the `filter` method to find field names that are not in 'internalFields'
        const filteredFields = allFields.filter((fieldName) => !internalFields.includes(fieldName));
        return filteredFields;
    }

    private addField(field: Field): void {
        // Add field
        const fieldName = field.getName();
        if (fieldName && this.externalFields[fieldName]) {
            const options = this.externalFields[fieldName];

            // Set ValueModelClass if a model option is given
            if (options.model) {
                field.setValueModelClass(options.model);
            }
        }

        this.fields[field.getName()] = field;
        // field.setOwningModelClass(this.constructor.name);
    }

    public getField(name: string): Field | null {
        // Get field
        return this.fields[name] !== undefined ? this.fields[name] : null;
    }

    public async fetchValues(): Promise<void> {
        this.initFields();
        await this.loadFieldValues();
    }

    private async loadFieldValues(): Promise<void> {
        for (const [fieldname, field] of Object.entries(this.fields)) {
            // extern fields via model relation
            if (this.externalFields.hasOwnProperty(fieldname)) {
                await this.loadExternal(fieldname);
                // dataset attributes itself
            } else {
                // Field is not external and gets handled by simply reading. to snake_case
                const property_name = this.convertFieldnameToColumn(fieldname); //id
                const fieldVal = this[property_name]; //276

                // Explicitly set null if the field represents a model except for dates.
                if (field.getValueModelClass() !== null) {
                    field.setValue(fieldVal === undefined || fieldVal === null ? null : fieldVal);
                } else {
                    field.setValue(fieldVal);
                }
            }
        }
    }

    private async loadExternal(fieldname: string): Promise<void> {
        const field = this.fields[fieldname];

        // let modelclass: typeof Title | typeof Description;
        let modelclass: DatasetRelatedModel = field.getValueModelClass();
        let modelInstance = new modelclass();

        // Create a query builder
        const select = modelclass.query();

        // If any declared constraints, add them to the query
        if (this.externalFields[fieldname]?.options) {
            const options: Array<string> = this.externalFields[fieldname].options;
            for (const [column, value] of Object.entries(options)) {
                if (Array.isArray(value)) {
                    select.whereIn(column, value);
                } else {
                    select.where(column, value);
                }
            }
        }

        // Get dependent rows
        const result: Record<string, any>[] = [];
        const datasetId = this.id;
        let rows: any[] = [];

        if (this.externalFields[fieldname]?.through) {
            const relation = this.externalFields[fieldname].relation;
            // rows = this[relation];
            rows = await this.related(relation).query();

            if (this.externalFields[fieldname].pivot) {
                const pivotArray = this.externalFields[fieldname].pivot;
                const pivotValue = pivotArray.role;
                // rows = this[relation]().wherePivot('role', pivotValue).get();
                rows = await this.related(relation).query().wherePivot('role', pivotValue);
            }
        } else if (modelInstance.hasOwnProperty('dataset')) {
            rows = await select
                .whereHas('dataset', (q) => {
                    q.where('id', datasetId);
                })
                .orderBy('id')
                .select();
        }

        // 1 ..n relations
        for (const row of rows) {
            const attributes = Object.keys(row.$attributes);

            if (this.externalFields[fieldname]?.pivot) {
                const pivotArray = this.externalFields[fieldname].pivot;
                const arrayKeys = Object.keys(pivotArray);
                const extendedArrayKeys = arrayKeys.map((pivotAttribute) => {
                    return `pivot_${pivotAttribute}`;
                });
                attributes.push(...extendedArrayKeys);
            }

            const objArray: Record<string, any> = {};

            for (const property_name of attributes) {
                let fieldName = this.convertColumnToFieldname(property_name);
                let fieldval = '';

                if (property_name.startsWith('pivot_')) {
                    const str = property_name.replace('pivot_', '');
                    fieldName = this.convertColumnToFieldname(str);
                    // fieldval = row.$pivot[str];
                    fieldval = row.$extras[property_name];
                } else if (fieldName === 'Type') {
                    fieldval = row[property_name]?.charAt(0).toUpperCase() + row[property_name]?.slice(1);
                } else {
                    fieldval = row[property_name];
                }

                objArray[fieldName] = fieldval;
            }

            result.push(objArray);
        }

        // Set the field value
        field.setValue(result);
    }

    // to snakeCase
    private convertFieldnameToColumn(fieldname: string): string {
        return fieldname.replace(/([a-z\d])([A-Z])/g, '$1_$2').toLowerCase();
    }

    private convertColumnToFieldname(columnName: string): string {
        return columnName
            .split(/[-_]/)
            .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
            .join('');
    }
}
