import { BASE_VARIANT, NangoActionBase } from './action.js';
import { validateData } from './dataValidation.js';

import type { ValidateDataError } from './dataValidation.js';
import type { RawModel, ZodCheckpoint, ZodMetadata, ZodModel } from './types.js';
import type { MaybePromise, NangoProps } from '@nangohq/types';
import type * as z from 'zod';

export abstract class NangoSyncBase<
    TModels extends Record<string, ZodModel> = never,
    TMetadata extends ZodMetadata = never,
    TCheckpoint extends ZodCheckpoint = never,
    TModelName extends keyof TModels = keyof TModels
> extends NangoActionBase<TMetadata, TCheckpoint> {
    public variant = BASE_VARIANT;

    lastSyncDate?: Date;
    track_deletes = false;
    emptyCache = false;

    constructor(config: NangoProps) {
        super(config);

        if (config.lastSyncDate) {
            this.lastSyncDate = config.lastSyncDate;
        }

        if (config.track_deletes) {
            this.track_deletes = config.track_deletes;
        }

        if (config.emptyCache) {
            this.emptyCache = config.emptyCache;
        }

        if (config.syncVariant) {
            this.variant = config.syncVariant;
        }
    }

    public modelFullName(model: string) {
        if (this.variant === BASE_VARIANT) {
            return model;
        }
        return `${model}::${this.variant}`;
    }

    /**
     * @deprecated please use batchSave
     */
    public async batchSend<TModel extends RawModel = z.infer<TModels[TModelName]>>(results: TModel[], model: TModelName): Promise<boolean | null> {
        return this.batchSave(results, model);
    }

    public abstract batchSave<TModel extends RawModel = z.infer<TModels[TModelName]>>(results: TModel[], model: TModelName): MaybePromise<boolean>;

    public abstract batchDelete<TModel extends RawModel = z.infer<TModels[TModelName]>>(
        results: (Pick<TModel, 'id'> & Partial<TModel>)[],
        model: TModelName
    ): MaybePromise<boolean>;

    public abstract batchUpdate<TModel extends RawModel = z.infer<TModels[TModelName]>>(
        results: (Pick<TModel, 'id'> & Partial<TModel>)[],
        model: TModelName
    ): MaybePromise<boolean>;

    public abstract getRecordsByIds<TKey = string | number, TModel extends RawModel = z.infer<TModels[TModelName]>>(
        ids: TKey[],
        model: TModelName
    ): MaybePromise<Map<TKey, TModel>>;

    public abstract listRecords<TModel extends RawModel = z.infer<TModels[TModelName]>>(
        model: TModelName,
        options?: {
            cursor?: string;
        }
    ): AsyncGenerator<TModel>;

    /**
     * @deprecated please use trackDeletesStart and trackDeletesEnd
     */
    public abstract deleteRecordsFromPreviousExecutions(model: TModelName): MaybePromise<{ deletedKeys: string[] }>;

    /*
     * Mark the start of a deletes tracking phase for a given model, if not already started.
     * During this phase, the sync will keep track of all records that are not present anymore in the saved data.
     */
    public abstract trackDeletesStart(model: TModelName): MaybePromise<void>;

    /*
     * Mark the end of a deletes tracking phase for a given model
     * All the records that are not present anymore in the saved data since the start of the tracking phase will be marked as deleted, and their keys will be returned.
     */
    public abstract trackDeletesEnd(model: TModelName): MaybePromise<{ deletedKeys: string[] }>;

    public abstract setMergingStrategy(merging: { strategy: 'ignore_if_modified_after' | 'override' }, model: TModelName): Promise<void>;

    protected validateRecords(model: string, records: unknown[]): { data: any; validation: ValidateDataError[] }[] {
        // Validate records
        const hasErrors: { data: any; validation: ValidateDataError[] }[] = [];
        for (const record of records) {
            const validation = validateData({
                version: this.syncConfig?.version || '1',
                input: JSON.parse(JSON.stringify(record)),
                jsonSchema: this.syncConfig!.models_json_schema,
                modelName: model
            });
            if (validation === true) {
                continue;
            }

            hasErrors.push({ data: record, validation });

            if (this.runnerFlags?.validateSyncRecords) {
                break;
            }
        }

        return hasErrors;
    }

    protected removeMetadata<T extends object>(results: T[]) {
        if (!Array.isArray(results)) {
            return results;
        }
        return results.map((result) => {
            if (result && typeof result === 'object' && '_nango_metadata' in result) {
                delete result._nango_metadata;
            }

            return result;
        });
    }
}