import { childLogger, Logger } from "@foxxmd/logging";
import {
    cacheFunctions,
} from "@foxxmd/regex-buddy-core";
import deepEqual from 'fast-deep-equal';
import { Simulate } from "react-dom/test-utils";
import { LifecycleStep, PlayData, PlayObject, TransformResult } from "../../core/Atomic.js";
import { buildPlayHumanDiffable, buildTrackString } from "../../core/StringUtils.js";
import { CommonClientConfig } from "./infrastructure/config/client/index.js";
import { CommonSourceConfig } from "./infrastructure/config/source/index.js";
import { mergeSimpleError, SkipTransformStageError, StagePrerequisiteError, StageTransformError, TransformRulesError } from "./errors/MSErrors.js";
import {
    PlayTransformRules,
    StageConfig,
    TRANSFORM_HOOK,
    TransformHook
} from "./infrastructure/Transform.js";
import AbstractInitializable from "./AbstractInitializable.js";
import play = Simulate.play;
import TransformerManager from "./transforms/TransformerManager.js";
import { getRoot } from "../ioc.js";
import { nanoid } from "nanoid";
import {diffStringsUnified, DiffOptionsColor} from 'jest-diff';
import chalk from 'chalk';
import { isDebugMode } from "../utils.js";
import { findCauseByReference } from "../utils/ErrorUtils.js";
import { hashObject } from "../utils/StringUtils.js";
import { metaInvariantTransform, playContentInvariantTransform } from "../utils/PlayComparisonUtils.js";
import { MSCache } from "./Cache.js";
import { jdiff } from "../../core/DataUtils.js";
import ConsoleFormatter from "jsondiffpatch/formatters/console";
import clone from "clone";

const console = new ConsoleFormatter();

export type AbstractComponentConfig = (CommonClientConfig | CommonSourceConfig) & { transformManager?: TransformerManager };

export default abstract class AbstractComponent extends AbstractInitializable {

    declare config: CommonClientConfig | CommonSourceConfig;

    transformRules: PlayTransformRules = {};
    regexCache!: ReturnType<typeof cacheFunctions>;
    protected transformManager: TransformerManager;
    protected cache: MSCache;

    protected constructor(config: AbstractComponentConfig) {
        super(config);
        this.transformManager = config.transformManager ?? getRoot().items.transformerManager;
        this.cache = getRoot().items.cache();
    }

    protected postCache(): Promise<void> {
        try {
            this.buildTransformRules();
            return;
        } catch (e) {
            throw e;
        }
    }

    public buildTransformRules() {
        this.logger.debug('Building transformer rules...');
        try {
            this.doBuildTransformRules();
        } catch (e) {
            this.buildOK = false;
            throw new TransformRulesError('Could not build playTransform rules. Check your configuration is valid.', {cause: e});
        }
        try {
            //const ruleCount = countRegexes(this.transformRules);
            this.regexCache = cacheFunctions(200);
        } catch (e) {
            this.logger.warn(new TransformRulesError('Failed to count number of rule regexes for caching but will continue will fallback to 100', {cause: e}));
        }
    }

    protected doBuildTransformRules() {
        const {
            options: {
                playTransform
            } = {}
        } = this.config;

        if (playTransform === undefined) {
            this.logger.debug(`No rules found under property 'playTransform'`);
            return;
        }

        const {
            preCompare: preConfig,
            compare: {
                candidate: candidateConfig,
                existing: existingConfig,
            } = {},
            postCompare: postConfig
        } = playTransform;

        let preCompare: StageConfig[],
            candidate: StageConfig[],
            existing: StageConfig[],
            postCompare: StageConfig[];

        const builtHooks: string[] = [];
        const emptyHooks: string[] = [];
        try {
            preCompare = this.transformPartToStrong(preConfig);
            if(preCompare === undefined) {
                emptyHooks.push('preCompare')
            } else {
                builtHooks.push(`preCompare => ${preCompare.map(x => `${x.type}${x.name !== undefined ? `-${x.name}` : ''}`)}`);
            }
        } catch (e) {
            throw new Error('preCompare was not valid', {cause: e});
        }

        try {
            candidate = this.transformPartToStrong(candidateConfig);
            if(candidate === undefined) {
                emptyHooks.push('candidate')
            } else {
                builtHooks.push(`canidate => ${candidate.map(x => `${x.type}${x.name !== undefined ? `-${x.name}` : ''}`)}`);
            }
        } catch (e) {
            throw new Error('candidate was not valid', {cause: e});
        }

        try {
            existing = this.transformPartToStrong(existingConfig);
             if(existing === undefined) {
                emptyHooks.push('existing')
            } else {
                builtHooks.push(`existing => ${existing.map(x => `${x.type}${x.name !== undefined ? `-${x.name}` : ''}`)}`);
            }
        } catch (e) {
            throw new Error('existing was not valid', {cause: e});
        }

        try {
            postCompare = this.transformPartToStrong(postConfig);
             if(postCompare === undefined) {
                emptyHooks.push('postCompare')
            } else {
                builtHooks.push(`postCompare => ${postCompare.map(x => `${x.type}${x.name !== undefined ? `-${x.name}` : ''}`)}`);
            }
        } catch (e) {
            throw new Error('postCompare was not valid', {cause: e});
        }

        this.logger.debug(`Hooks built! Empty: ${emptyHooks.join(', ')} | Configured: ${builtHooks.length === 0 ? 'None' : `\n${builtHooks.join('\n')}`}`);

        this.transformRules = {
            preCompare,
            compare: {
                candidate,
                existing,
            },
            postCompare,
        }
    }

    protected transformPartToStrong(data: any) {
        if(data === undefined) {
            return undefined;
        }
        // default to user transform type for backward compatibility
        const partArr = (Array.isArray(data) ? data : [data]).map(x => ({type: 'user', ...x}));

        return partArr.map(x => this.transformManager.parseTransformerConfig(x));
    }

    public transformPlay = async (play: PlayObject, hookType: TransformHook, log?: boolean) => {


        const asyncId = nanoid(6);
        let logger = childLogger(this.logger, ['Play Transform', hookType, asyncId]);

        try {
            let hook: StageConfig[];

            switch (hookType) {
                case TRANSFORM_HOOK.preCompare:
                    hook = this.transformRules.preCompare;
                    break;
                case TRANSFORM_HOOK.candidate:
                    hook = this.transformRules.compare?.candidate;
                    break;
                case TRANSFORM_HOOK.existing:
                    hook = this.transformRules.compare?.existing;
                    break;
                case TRANSFORM_HOOK.postCompare:
                    hook = this.transformRules.postCompare;
                    break;
            }

            if (hook === undefined) {
                return play;
            }

            const shouldLog = log ?? this.config.options?.playTransform?.log ?? isDebugMode();

            const transformHash = `playTransform-${hashObject(hook)}-${hashObject(playContentInvariantTransform(play))}`;
            const cachedTransformPlay = await this.cache.cacheTransform.get<PlayObject>(transformHash);
            if(cachedTransformPlay !== undefined) {
                // if(shouldLog) {
                //     logger.debug(`Used cached Transform for => ${buildTrackString(play)}`);
                // }
                //logger.debug(`Cache hit ${transformHash}`);
                return cachedTransformPlay;
            }

            logger.debug(`Transform start for => ${buildTrackString(play)}`);
            let transformedPlay: PlayObject = play;
            let transformHistory: TransformResult[] = [];
            let shouldBreak = false;
            for(const hookItem of hook) {

                const {
                    onSuccess = 'continue',
                    onFailure = 'stop',
                    onSkip = 'continue',
                    failureReturnPartial = false
                } = hookItem;

                const existingStepIndex = transformedPlay.meta.lifecycle.steps.findIndex(x => x.name === 'hookType' && x.source === this.getIdentifier());
                const step: LifecycleStep = existingStepIndex !== -1 ? transformedPlay.meta.lifecycle.steps[existingStepIndex] : {
                    name: hookType,
                    source: this.getIdentifier(),
                }

                let newTransformedPlay: PlayObject,
                stageName: string = 'Unnamed',
                err: Error,
                cacheOk: boolean = true;
                try {
                    [newTransformedPlay, stageName] = await this.transformManager.handleStage(hookItem, transformedPlay, asyncId);
                } catch (e) {
                    err = e;
                    if(e instanceof StageTransformError) {
                        stageName = e.stageName;
                    }
                }

                if(err !== undefined) {
                    const merged = mergeSimpleError(err);
                    step.error = merged;

                    const skipError = findCauseByReference(err, SkipTransformStageError);
                    if(skipError !== undefined) {
                        let skipMsg = `Stage '${stageName}' was skipped`;
                        step.flowResult = onSkip;
                        step.flowKnownState = 'skip';

                        if(onSkip === 'stop') {
                            skipMsg += ' and will stop transform due to onSkip: stop';
                        }
                        step.flowReason = skipMsg;

                        logger.debug(merged, skipMsg);
                        if(onSkip === 'stop') {
                            shouldBreak = true;
                        }
                    } else if(onFailure === 'continue') {
                        logger.warn(merged, 'Transform encountered an error but continuing due to onFailure: continue');
                        step.flowReason = 'Transform encountered an error but continuing due to onFailure: continue';
                        step.flowResult = onFailure;
                    } else {
                        step.flowResult = onFailure;
                        const reqError = findCauseByReference(err, StagePrerequisiteError);
                        if(reqError !== undefined) {
                            const prereqMsg = 'Transform could not be completed due to prerequisite failure';
                            logger.warn(merged, prereqMsg);
                            step.flowKnownState = 'prereq';
                            step.flowReason = prereqMsg;
                        } else {
                            logger.error(merged, 'Transform encountered an error');
                            cacheOk = false;
                            step.flowReason = 'Transform encountered an error';
                        }

                        if(!failureReturnPartial) {
                            // rewind to original play so we don't return partial transform
                            transformedPlay = play;
                            transformHistory = [];
                        } else if(transformHistory.length > 0) {
                            step.flowReason += ' | Preserving play transformations up to this point due to failureReturnPartial=true'
                        }
                        shouldBreak = true;
                    }
                } else {
                    step.flowResult = onSuccess;

                    if(!deepEqual(play, newTransformedPlay)) {
                        const o = JSON.parse(JSON.stringify(play));
                        const t = JSON.parse(JSON.stringify(newTransformedPlay));
                        const patch = jdiff.diff(o,t);
                        step.patch = patch;
                    }

                    if(newTransformedPlay.meta.lifecycleInputs?.length > 0) {
                        step.inputs = clone(newTransformedPlay.meta.lifecycleInputs)
                    } else if(transformedPlay.meta.lifecycleInputs?.length > 0) {
                        step.inputs = clone(transformedPlay.meta.lifecycleInputs)
                    }
                    
                    delete transformedPlay.meta.lifecycleInputs;

                    // for logging only
                    transformHistory.push({
                        type: hookItem.type,
                        name: stageName,
                        play: newTransformedPlay.data
                    });
                    transformedPlay = newTransformedPlay;
                }

                if(err === undefined && onSuccess === 'stop') {
                    logger.debug(`Stopping transform due to onSuccess: stop`);
                    shouldBreak = true;
                }

                if(existingStepIndex !== -1) {
                    transformedPlay.meta.lifecycle.steps[existingStepIndex] = step;
                } else {
                    transformedPlay.meta.lifecycle.steps.push(step);
                }

                if(shouldBreak) {
                    break;
                }
            }

            if(shouldLog !== false) {
                if(transformHistory.length === 0) {
                    logger.debug('Transform Diff: No Change');
                } else {
                    const historyToDiff: {name: string, data: PlayData}[] = [
                        {name: 'Original', data: play.data}
                    ];
                    if(shouldLog === true) {
                        const last = transformHistory[transformHistory.length - 1];
                        historyToDiff.push({name: `${last.type}-${last.name}`, data: last.play});
                    } else {
                        for(const t of transformHistory) {
                            historyToDiff.push({name: `${t.type}-${t.name}`, data: t.play})
                        }
                    }
                    const diffs: string[] = [];
                    historyToDiff.forEach((curr, index) => {
                        if(index === 0) {
                            return;
                        }
                        const last = historyToDiff[index - 1];
                        if(deepEqual(last.data, curr.data)) {
                            diffs.push(`${last.name} => ${curr.name} -- No Change`);
                        } else {
                            const unchanged = index - 1 === 0 ? last.data : undefined;

                            const formattedDiff = console.format(jdiff.diff(last.data, curr.data), unchanged);
                            diffs.push(`${last.name} => ${curr.name}\n${formattedDiff}`);
                        }
                    });
                    logger.debug(`Transform Diff\n${diffs.join('\n')}`);
                }
            }

            await this.cache.cacheTransform.set<PlayObject>(transformHash, transformedPlay, '10m');
            return transformedPlay;
        } catch (e) {
            logger.warn(new Error(`Unexpected error occurred, returning original play.`, {cause: e}));
            return play;
        }
    }
}
