import {Testable} from "./Testable";
import {TestableNode} from "./TestableNode";
import {
    LastRenderedNodeData,
    MiddleRenderedNodeData,
    RendererNode,
    RendererNodeData,
    RendererNodeType,
    TreeNodeComponent
} from "./RendererNodeData";
import {AfterRemovingFromGroveHook, getGroveForReading, TreeNodeData} from "./Grove";
import {AddedToTreeNodeHook, RemovedFromTreeNodeHook, TreeNode} from "./TreeNode";
import {Offers} from "./Offers";
import {WithTestableNode} from "./WithTestableNode";
import {ExportableToState} from "./ExportableToState";
import {FlowDataCreator, FlowDataWithoutPosition} from "./FlowDataCreator";
import {OfferData, TestableData} from "./store";
import mitt from "mitt";
import {NestedTestableNodeData} from "./InMemoryTestableNode";
import {cloneDeep, omitBy} from "lodash";
import {TierType} from "./tiers";
import {NestedBranchData} from "./MemoryBranchNode";
import {Testables} from "./testables";
import {DiscountOptions} from "./cards/Discount";
import {TestableCompositeNode} from "./TestableCompositeNode";
import {AddTierButton} from "./AddTierButton";
import {offers} from "../conditionsOrFilters/CardCreator";
import {removeIds} from "./NodeHelpers";
import {getComponentMeta} from "./ComponentsMeta";
import {DataExporter, ExportImportTarget} from "./export/Exporters";
import {exporterImporters} from "./export/CustomImportersAndExporters";
import {GroveExporter} from "./export/GroveExporter";
import {currentPreloadedDynamicData} from "./export/ComponentExporterImporter";

export type BranchData<ExtraData extends object = {}, ChildrenType = TreeNodeData | string | any> = {
    id: string,
    type: 'select-action' | 'offers' | 'tiered',
    data?: ExtraData,
    children: ChildrenType[]
}

export type TieredExtraData<TestableFormat = NestedTestableNodeData | string> = {
    baseTestable: TestableFormat,
    tierType: TierType
}

export interface BranchNode<TestableType extends Testable = Testable, ChildrenType extends object = any> extends RendererNodeData, LastRenderedNodeData,
    TreeNodeComponent,
    WithTestableNode,
    Partial<AfterRemovingFromGroveHook>,
    Partial<RemovedFromTreeNodeHook>,
    Partial<AddedToTreeNodeHook> {
    getChildrenIds(): string[]

    getChildren<In = any, Out = any>(factory?: (child: In, branch: BranchNode) => Out | undefined): ChildrenType[]

    // replaces the children with the new ones
    //setChildren(children: ChildrenType[]): void
    getType(): BranchData['type']

    isOffers(): boolean

    isSelectAction(): boolean

    isTiered(): boolean

    isSimple(): boolean

    isInsideAnotherBranch(): boolean

    addChildren(childrenData: ChildrenType[]): void
}

export type TieredData = Pick<TieredExtraData, 'tierType'> & {
    baseTestableId: string,
}

export enum BranchNodeEvent {
    ChildrenAdded = 'children.added',
    ChildrenRemoved = 'children.removed',
}

export type BranchNodeEventTypes<ChildrenType> = {
    [BranchNodeEvent.ChildrenAdded]: {
        // the id of the branch
        branch: BranchNode,
        children: ChildrenType[],
    },
    [BranchNodeEvent.ChildrenRemoved]: {
        // the id of the branch
        branch: BranchNode,
    }
}

export type CenteredId = { id: string, native: boolean };

export class StateBranchNode<TestableNodeType extends TestableNode = TestableNode, ChildrenType extends any = BranchData['children'][number]> implements BranchNode,
    MiddleRenderedNodeData,
    RendererNode,
    ExportableToState<BranchData>,
    FlowDataCreator,
    AfterRemovingFromGroveHook,
    RemovedFromTreeNodeHook,
    AddedToTreeNodeHook,
    DataExporter<NestedBranchData>
{
    private emitter = mitt<BranchNodeEventTypes<ChildrenType>>()
    // @ts-ignore
    public testableNode: TestableNodeType
    // @ts-ignore
    public testableBaseNode: TestableNodeType;
    // @ts-ignore
    private treeNode: TreeNode;

    constructor(
        private readonly offers: Offers,
        public readonly data: BranchData,
        public readonly testables: Testables
    ) {
    }

    setTreeNode(treeNode: TreeNode): void {
        this.treeNode = treeNode;
    }

    getTreeNode(): TreeNode {
        return this.treeNode;
    }

    setTestable(testable: TestableNodeType) {
        this.testableNode = testable;
    }

    setTestableBase(testable: TestableNodeType) {
        this.testableBaseNode = testable;
        this.testableBaseNode.setTreeNode(this.treeNode);
        // @ts-ignore
        (this.testableBaseNode as unknown as TestableCompositeNode).branch = this // dont use setBranch() because that will set the testableNode and we don't want that
    }

    getId(): string {
        return this.data.id;
    }

    getCenteredId(): CenteredId | undefined {
        if (this.isOffers()) {
            // we want to get the id of the first offer card, not the whole offer because the offer has content outside of the cart and we only want to use the cared for centering
            return {
                id: this.getChildrenIds()[0],
                native: true // otherwise use a data-node-id attribute from react-flow (which is the default)
            }
        }
    }

    getSourceRenderNode(): RendererNodeType | undefined {
        return this.testableNode
    }

    getSourceRenderNodeId(): string {
        return this.getSourceRenderNode()?.getId() || ''
    }

    getTargetRenderNode(): RendererNodeType | undefined {
        if (this.isTiered()) {
            return this.testableBaseNode
        }
    }

    getTargetRenderNodeId(): string {
        return this.getTargetRenderNode()?.getId() || ''
    }

    /**
     * I know, i know, this should be in a different class because not all branches have a base testable
     */
    getBaseTestable(): TestableData | undefined {
        return this.testables.getNode(this.getExtraData().baseTestableId);
    }

    getChildrenIds(): string[] {
        if (this.isTiered()) {
            return (this.data.children as BranchData<{}, TreeNodeData>['children']).map(({id}) => id)
        }
        // @ts-ignore
        return this.data.children;
    }

    getFirstChildId(): string {
        return this.getChildrenIds()[0]
    }

    getLastChildId(): string {
        const childrenIds = this.getChildrenIds();

        return childrenIds[childrenIds.length - 1];
    }

    isFirstChild<T extends ChildrenType | TreeNode = ChildrenType>(child: T): boolean {
        if (this.isTiered()) {
            return (child as TreeNode).id === this.getFirstChildId();
        }
        return child === this.getFirstChildId();
    }

    getChildren<In = any, Out extends any = ChildrenType>(factory?: (child: In, branch) => Out | undefined): Out[] {
        // for offers, use: Offers.get(child as string)
        // for tiered, use: Grove.get<TreeNode>(child as TreeNodeData.id)
        return this.data.children.map((child) => {
            return factory!(child as In, this) as unknown as Out;
        }).filter(v => v !== undefined)
    }

    getChildIndex(childId: string): number {
        return this.getChildrenIds().indexOf(childId)
    }

    getChildAtIndex<ReturnedChildren extends any = ChildrenType>(index: number, factory?: (child: ChildrenType) => ReturnedChildren): ReturnedChildren | undefined {
        const rawChild = this.data.children[index]

        if (!rawChild) {
            return undefined
        }

        if (factory) {
            return factory(rawChild as ChildrenType) as ReturnedChildren
        }

        return rawChild as ReturnedChildren
    }

    getAddTierButton(): AddTierButton | undefined {
        if (this.isTiered()) {
            return new AddTierButton(
                this.getAddTierButtonId(),
                this.treeNode,
                this.treeNode.grove
            )
        }
    }

    /**
     * Returns the *original reference* to the data
     */

    isOffers(): boolean {
        return this.data.type === 'offers';
    }

    isSelectAction(): boolean {
        return this.data.type === 'select-action';
    }

    isTiered(): boolean {
        return this.data.type === 'tiered';
    }

    isSimple(): boolean {
        return this.isOffers() || this.isSelectAction();
    }

    getType() {
        return this.data.type;
    }

    isInsideAnotherBranch() {
        return this.getTreeNode().hasParent();
    }

    exportToState() {
        // we can use the global grove here because it's for read operations only
        const grove = getGroveForReading();

        return omitBy(
            {
                id: this.getId(),
                type: this.getType(),
                data: this.getExtraData(),
                children: this.isTiered() ? this.getChildren((child) => {
                    return child;
                }) : this.getChildrenIds()
            },
            v => v === undefined
        )
    }

    createFlowData(): FlowDataWithoutPosition {
        const tieredFlowData = this.getTieredFlowNodes();

        return {
            renderedNodes: [
                // the actual branch node
                this.treeNode.createRenderedNodeWithData({
                    id: this.getId(),
                    type: this.isSelectAction() ? 'branch.select-action-split' : (this.isOffers() ? 'branch.offers' : 'branch.tiered'),
                }),
                // the tiered nodes (base testable and children)
                ...(tieredFlowData.renderedNodes)
            ],
            edges: tieredFlowData.edges
        };
    }

    private getTieredFlowNodes(): FlowDataWithoutPosition {
        if (!this.isTiered() || this.data.children.length === 0) {
            return {renderedNodes: [], edges: []}
        }

        const childrenTreeNodes = this.getChildren<BranchData, TreeNode>(({id}) => {
            return this.treeNode.grove.get<TreeNode>(id)
        }) as unknown as TreeNode[]

        return {
            renderedNodes: [
                // the base testable
                this.treeNode.createRenderedNodeWithData({
                    id: (this.getExtraData() as TieredData).baseTestableId,
                    type: 'branch.tiered.testable.base',
                }),
                // the children
                ...childrenTreeNodes.map(
                    treeNode => treeNode.createFlowData().renderedNodes
                ).flat(),
                // the add tier button
                this.treeNode.createRenderedNodeWithData({
                    id: this.getAddTierButtonId(),
                    type: 'branch.tiered.add-tier-button'
                })
            ],
            edges: [
                // the branch to the base testable
                {
                    id: this.getId(),
                    sourceHandle: 'right',
                    targetHandle: 'left',
                    source: this.getId(),
                    target: this.getExtraData().baseTestableId,
                    type: 'connecting-line',
                },
                // the base testable to the first tier's step anchor
                {
                    id: this.getExtraData().baseTestableId,
                    sourceHandle: 'right',
                    targetHandle: 'left',
                    source: this.getExtraData().baseTestableId,
                    // the first tree node id, which represents the step anchor
                    target: this.getChildAtIndex<TreeNodeData>(0)!.id,
                    type: 'connecting-line',
                },
                // the step anchor to the tier testable is done in the TreeNode
                // the add tier button
                {
                    id: this.getAddTierButtonId(),
                    targetHandle: 'top',
                    sourceHandle: 'bottom',
                    source: this.getLastChildId(),
                    target: this.getAddTierButtonId(),
                    type: 'connecting-line',
                }
            ]
        }
    }

    private getAddTierButtonId() {
        return `${this.getId()}.add-tier-button`;
    }

    public getExtraData<Data extends unknown = TieredData>(): Data {
        return this.data.data as Data;
    }

    /**
     *
     * CURRENTLY ONLY WORKS FOR SHALLOW BRANCHES (OFFERS, SELECT-ACTION)
     *
     *
     * Difference between this.data and exportNestedData is that this.data is the *state* data (eg: offer children are strings) whereas
     * nested data is the *actual* data (eg: offer children are objects)
     */
    public exportData(target: ExportImportTarget = 'preset', groveExporter?: GroveExporter): NestedBranchData {
        // make sure we return a copy of the data
        let branchData = cloneDeep({
                ...this.data,
                children: this.getChildren(
                    (child) => {
                        if (this.isTiered()) {
                            if (target === 'clone-tier') {
                                // logic is handled elsewhere
                                return child
                            }
                            /**
                             * Children are TreeNodes so let's export those recursively
                             */
                            const childTreeNode = this.treeNode.grove.get<TreeNode>(child.id)

                            if (childTreeNode && groveExporter) {
                                return groveExporter.exportTreeNode(
                                    childTreeNode,
                                    target
                                )
                            }
                            // else it will return undefined which the parent function will filter out
                        }

                        let offer = cloneDeep(this.offers.get(child as string));
                        const offerMeta = getComponentMeta('offers', offer!.type!)
                        const customExportedOffer = exporterImporters[offer!.type]?.exportData?.(offer, target)
                        /*const customExportedOffer = exporters[offer!.type]?.(offer, target)*/

                        if (customExportedOffer) {
                            offer = customExportedOffer as OfferData
                        }

                        if (target === 'server') {
                            const preloadedValues = exporterImporters[offer.type]?.tagDynamicValues?.(offer, 'server')

                            if (preloadedValues) {
                                currentPreloadedDynamicData.addPreloadedDynamicData(preloadedValues)
                            }
                        }

                        if (target === 'preset') {
                            // gotta remove the ids
                            // @ts-ignore
                            offer = removeIds(offer) as CardData
                            // we also gotta remove the ids from the tetsables of the get y offer
                        }

                        return offer as unknown as ChildrenType
                    }
                )
            }
        )

        if (this.isTiered()) {
            // we need to convert the extra data testable from id to the actual object
            const tierExtraData = branchData.data as TieredData
            const exportedTierExtraData = tierExtraData as unknown as TieredExtraData
            const testable = new Testable(this.testables, tierExtraData.baseTestableId)

            exportedTierExtraData.baseTestable = testable.export(target)
            // @ts-ignore
            delete tierExtraData.baseTestableId;
        }

        if (target === 'preset') {
            // @ts-ignore
            branchData = removeIds<BranchData>(branchData)
        }

        return branchData
    }

    leftSpacing(): number {
        if (this.isTiered()) {
            if (this.isInsideAnotherBranch()) {
                return 32
            } else {
                return 20
            }
        } else if (this.isInsideAnotherBranch()) {
            return 40
        }

        return 0
    }

    topSpacing(): number {
        if (this.isTiered()) {
            if (this.isInsideAnotherBranch()) {
                return 32
            } else {
                return 24 * 4
            }
        } else if (this.treeNode.isRoot()) {
            if (this.isSelectAction()) {
                return 32
            }

            return 32 * 2
        }

        return 0
    }

    afterRemovingFromGrove() {
        this.beforeRemovingFromTreeNode()
    }

    beforeRemovingFromTreeNode() {
        // remove the children
        if (this.isOffers()) {
            this.getChildrenIds().forEach((id) => {
                const offerToRemove = this.offers.get(id);

                this.offers.remove(id);

                if (offerToRemove?.type === 'Discount') {
                    const options = offerToRemove.options as DiscountOptions;

                    if (typeof options.customFilteredItemsSourceMap === 'string') {
                        this.testables.remove(options.customFilteredItemsSourceMap);
                    }
                }
            });
        }
    }

    afterAddingToTreeNode() {
        // add the offers to the state
        // this is where we'll add the offers to the state
        // and deal with tiered state
        // this should probably be done in a different class for each type of branch
        // but for now, let's do it here
        if (this.isOffers()) {
            this.offers.add(this.getChildren(this.offers.get.bind(this.offers)) as OfferData[])
        }
    }

    addSubscriber(event: BranchNodeEvent, callback: (data: BranchNodeEventTypes<ChildrenType>[typeof event]) => void) {
        this.emitter.on(event, callback);
    }

    /**
     * If a tiered branch, we expect a full TreeNode object with all its data already in state
     * @param childrenData
     */
    addChildren(childrenData: (TreeNode | (OfferData | Omit<OfferData, 'id'>))[], position?: number) {
        if (childrenData.length === 0) {
            return
        }

        if (this.isOffers()) {
            this.offers.add(childrenData as unknown as OfferData[])
            this.data.children.push(...childrenData.map((child) => child.id))
        } else if (this.isTiered()) {
            (childrenData as unknown as TreeNode[]).forEach((child) => {
                // add at the position (1-based)
                if (typeof position === 'number') {
                    this.data.children.splice(position - 1, 0, child.exportToState())
                } else {
                    this.data.children.push(child.exportToState())
                }
            })
        }


        this.emitter.emit(BranchNodeEvent.ChildrenAdded, {
            branch: this,
            // @ts-ignore
            children: childrenData
        })
    }

    // for offers
    removeChildren(ids: string[]): void
    // for tiered branches
    removeChildren(children: TreeNode[]): void
    removeChildren(children: (string | TreeNode)[]): void {
        if (children.length === 0) {
            return
        }

        if (this.isOffers()) {
            this.data.children = this.data.children.filter((id) => {
                return !children.includes(id as string)
            })

            children.forEach(id => this.offers.remove(id as string))
        } else if (this.isTiered()) {
            children.forEach((child) => {
                const id = (child as TreeNode).id;

                (child as TreeNode).beforeRemovingFromGrove()

                this.data.children = this.data.children.filter((stateChild) => {
                    return (stateChild as TreeNodeData).id !== id
                })
            })
        }

        this.emitter.emit(BranchNodeEvent.ChildrenRemoved, {
            branch: this,
        })
    }
}