import {
    NodesStore,
    OfferData,
    RenderedNode,
    TestableData,
    TestablePartial, useNodesStore, WithParentNodeId,
    WithTargetNodeId,
    WithXSpacing
} from "./store";
import {Testables, TestablesState} from "./testables";
import {generate} from "shortid";
import {cloneDeep, merge, pickBy} from "lodash";
import {nodeTypes} from "../Tree";
import {StepAnchorData} from "../StepAnchorBranch";
import {OffersBranchData} from "../OffersBranch";
import {test} from "vitest";
import {Position} from "@xyflow/react";
import {increase, TierType} from "./tiers";

export const getTree = () => {
    const state = useNodesStore.getState();

    return new Tree(state, new Testables(state))
};

export type TreeState = Pick<NodesStore, 'renderedNodes' | 'edges' | 'offers' | 'branchRelations' | 'couponsPlusNodeTypes'>;

export class Tree {
    private isInsideATransaction: boolean = false;
    private afterPerformActionsQueue: (() => void)[] = [];

    constructor(
        public state: TreeState,
        public readonly testables: Testables
    ) {}

    /**
     * will remove rendered nodes and its related nodes from a testable id
     */
    removeNodeFromTestable(testableId: string) {
        const renderedNode = this.state.renderedNodes.find(node => node.data?.testableID === testableId);

        if (renderedNode) {
            this.removeRenderedNode(renderedNode.id);
        }
    }

    public removeRenderedNode(id: string) {
        this.perform('removeNode', {data: {id}})
    }

    private performRemoveNode({id}: {id: string}) {
        const nodeToRemove = this.getRenderedNode(id);
        const parentBranch = this.getParentBranch(id);
        //this need to be checked here before the node gets removed
        if (!nodeToRemove) {
            return;
        }

        const isStepAnchor = this.isStepAnchor(nodeToRemove.id);

        // before removing the node, let's remove its branch relations since the other methods depend on the object being there
        if (this.isTestablePartial(nodeToRemove.id) || this.isTreeNode(nodeToRemove.id)) {
            // le's remove the branch relations
            this.removeBranchRelations(nodeToRemove.id)
        }

        // remove the offers if this is an offers branch
        if (this.isOffersBranch(nodeToRemove.id)) {
            this.removeOffersBranch(nodeToRemove.id);
        }

        this.removeCouponsPlusNodeType(nodeToRemove.id);

        this.state.renderedNodes.splice(this.state.renderedNodes.indexOf(nodeToRemove), 1);
        // now if it has a target node, we need to remove it too
        const sourceNode = !isStepAnchor &&  this.getSourceNode(nodeToRemove.id);

        if (sourceNode) {
            this.removeRenderedNode(sourceNode.id);
        }

        if (this.getRenderedNode(`${nodeToRemove.id}.step-anchor`)) {
            this.removeRenderedNode(`${nodeToRemove.id}.step-anchor`);
        }

        if (nodeToRemove.type === 'tiered-base-node') {
            // its a tier branch so let's make sure we remove its add tier button
            const addTierButton = this.getAddTierButtonNode(nodeToRemove.id);

            if (addTierButton) {
                this.removeRenderedNode(addTierButton.id);
            }
        }

        const targetNode = nodeToRemove.data?.targetNodeId;

        if (targetNode) {
            this.removeRenderedNode(targetNode);
        }
        //update the add node button
        if (isStepAnchor) {
            this.updateAddTierButton(this.getParentBranchId(nodeToRemove.data?.sourceNodeId || ''));
        }
        // then we gotta remove the edges associated with that rendered node
        this.removeRenderedNodeEdge(nodeToRemove.id);
        // remove branch relations
        this.removeBranchChildren(parentBranch?.id, [nodeToRemove.id]);

        this.updateTieredBranchRelationships(parentBranch?.id)
        this.afterPerformActionsQueue.push(() => {
            this.addSelectActionBranchToSingleRootNodes()
        })
    }

    private getSourceNode(id: string): RenderedNode | undefined {
        return this.state.renderedNodes.find(({data: {sourceNodeId}}) => sourceNodeId === id);
    }

    private removeRenderedNodeEdge(id: string) {
        const edgeIndex = this.state.edges.findIndex(edge => edge.source === id || edge.target === id);

        if (edgeIndex !== -1) {
            this.state.edges.splice(edgeIndex, 1);
        }
    }

    removeOrphanNodes() {
        // if a tree node with a testableid and the testableid does not exist
        const renderedNodesWithATestable = this.state.renderedNodes.filter(renderedNode => {
            return this.isRootTreeNode(renderedNode.id);
            //return typeof renderedNode.data?.testableID === 'string'
        })

        const nodesWithoutValidTestables = renderedNodesWithATestable.filter(renderedNode=> {
            return !this.testables.getNode(renderedNode?.data?.testableID || '');
        })


        nodesWithoutValidTestables.forEach(({id}) => {
            this.removeRenderedNode(id);
        })

        const tierBaseNodesWithoutSourceTestable = this.state.renderedNodes.filter(
            node => {
                return node.type === 'tiered-base-node' && node.data?.testableID && !this.testables.getNode(node.data.testableID)
            }
        );

        tierBaseNodesWithoutSourceTestable.forEach(({id}) => {
            this.removeRenderedNode(id);
        })

        //remove add node buttons
        const addNodeButtons = this.state.renderedNodes.filter(node => node.type === 'branch-add-node-button');

        addNodeButtons.forEach(({id, data: {parentNodeId}}) => {
            if (!this.getRenderedNode(parentNodeId)) {
                this.removeRenderedNode(id);
            }
        })
    }

    createRootTree(testableCompositeGroupID: string) {
        // so, let's create a simple tree
        const rootNode = this.testables.getNode(testableCompositeGroupID);
        if (!rootNode) {
            return;
        }

        const rootNodeId = rootNode.id;
        const rootNodeData = this.state.renderedNodes.find(node => node.id === rootNodeId);
        if (rootNodeData) {
            return;
        }

        // add the tree-node rendred node
        this.state.renderedNodes.push({
            id: `${testableCompositeGroupID}->testable`,
            type: 'tree-node',
            data: {
                renderedNodeType: 'root',
                isDefaultAND: true,
                testableID: testableCompositeGroupID,
            },
            position: {
                x: 0,
                y: 0
            }
        })
        // add the branch rendered node
        this.addSelectActionBranch(testableCompositeGroupID);

        // now add the edge
        this.state.edges.push({
            id: testableCompositeGroupID,
            source: `${testableCompositeGroupID}->testable`,
            target: `${testableCompositeGroupID}->branch`,
            type: 'connecting-line',
        })
        // that's it for now
    }

    private addSelectActionBranch(testableCompositeGroupIDorTestablePartialID: string) {
        let sourceId: string,
            isInsideBranch: boolean = false

        if (this.isTestablePartial(testableCompositeGroupIDorTestablePartialID)) {
            // here the source is the testable partial itself
            sourceId = testableCompositeGroupIDorTestablePartialID
            isInsideBranch = true
        } else {
            // here it's the testable composite group
            sourceId = this.getSuffixed(testableCompositeGroupIDorTestablePartialID, 'testable')
        }

        const branchId = this.getSuffixed(testableCompositeGroupIDorTestablePartialID, 'branch');

        if (this.getRenderedNode(branchId)) {
            // if it already exists, we don't need to add it again
            return;
        }

        this.state.renderedNodes.push({
            id: branchId,
            type: 'select-action-branch',
            data: {
                renderedNodeType: 'action',
                isInsideBranch,
                sourceNodeId: sourceId
            },
            position: {
                x: 0,
                y: 0
            },
        })

        this.addChildrenRelations(sourceId, [branchId]);
        this.setCouponsPlusNodeType(branchId, 'select-action')
    }

    private getSuffixed(id: string, suffix: 'testable' | 'branch') : string {
        if (suffix === 'testable') {
            // if it ends with ->testable, do noting
            if (id.endsWith('->testable')) {
                return id;
            } else if (id.endsWith('->branch')) {
                // if it ends with ->branch, we need to remove the branch
                return id.replace(/->branch$/, '->testable');
            } else {
                // if it doesn't end with ->testable or ->branch, we need to add the testable
                return id + '->testable';
            }
        } else if (suffix === 'branch') {
            // if it ends with ->branch, do nothing
            if (id.endsWith('->branch')) {
                return id;
            } else if (id.endsWith('->testable')) {
                // if it ends with ->testable, we need to remove the testable
                return id.replace(/->testable$/, '->branch');
            } else {
                // if it doesn't end with ->testable or ->branch, we need to add the branch
                return id + '->branch';
            }
        }

        return id;
    }

    /**
     * A valid target branch id is needed
     */
    addOffers(offers: Omit<OfferData, 'id'>[], targetBranchId: string) {
        offers.forEach(offer => {
            const branch = this.getRenderedNode(targetBranchId);

            if (!branch) {
                return;
            }

            const offerToAdd = {
                id: `offer-${generate()}`,
                ...offer, // this way we can keep an id if it had one
            };
            this.state.offers.push(offerToAdd)
            // now we need to add the offer to the branch
            // but what if the branch already has offers?
            const branchRelations = this.getBranchRelations(targetBranchId);
            if (branchRelations) {
                // just add the offer to the children
                branchRelations.children.push(offerToAdd.id);
            } else {
                // we aint have any relations, so we need to create one
                this.state.branchRelations.push({
                    branch: targetBranchId,
                    children: [
                        offerToAdd.id
                    ],
                    childrenType: 'offers'
                })
            }

            // alright now let's change the rendered node to an offers branch
            this.ensureIsAnOffersBranch(branch.id);
        })
    }

    public getRenderedNode(id: string): RenderedNode | undefined {
        return this.state.renderedNodes.find(node => node.id === id);
    }

    getBranchChildrenIds(renderedNodeId: string, withRelations: boolean = false): (string[] | [string[], (NodesStore['branchRelations'][number] | undefined)]) {
        if (/*this.isBaseTestable(branchId) ||*/ this.isTestablePartial(renderedNodeId) || this.isStepAnchor(renderedNodeId) || this.isAddTierButton(renderedNodeId)) {
            renderedNodeId = this.getParentBranchId(renderedNodeId) || renderedNodeId;
        }

        const branchRelations = this.state.branchRelations.find(branch => branch.branch === renderedNodeId);
        if (!branchRelations) {
            return withRelations? [[], undefined] : [];
        }

        return withRelations? [branchRelations.children, branchRelations] : branchRelations.children
    }

    public getBranchChildren<ChildrenDataType = RenderedNode | any>(branchId: string): ChildrenDataType[] {
        const [branchChildrenIds, branchRelations] = this.getBranchChildrenIds(branchId, true) as [string[], NodesStore['branchRelations'][number]]

        if (!branchRelations) {
            return [];
        }

        if (branchRelations.childrenType === 'offers') {
            // look in offers
            return branchRelations.children.map(childId => {
                const offer = this.state.offers.find(offer => offer.id === childId);

                if (offer) {
                    return offer as ChildrenDataType;
                }
            }).filter(Boolean) as ChildrenDataType[];
        }

        return branchChildrenIds.map(this.getRenderedNode.bind(this)).filter(Boolean) as ChildrenDataType[];
    }

    /**
     * Returns the parent id eg: the testable base if a tier
     */
    public getParentBranchId(nodeId: string) {
        const node = this.getRenderedNode(nodeId);

        if (!node) {
            return;
        }

        if (this.isBaseTestable(nodeId)) {
            return node.id;
        } else if (this.isTestablePartial(nodeId)) {
            return this.getParentBranchId(node.data.parentNodeId);
        } else if (this.isStepAnchor(nodeId)) {
            return this.getParentBranchId(
                (node.data.type === 'tiered-base' ? node.data.targetNodeId : node.data.sourceNodeId) || ''
            );
        } else if (this.isAddTierButton(nodeId)) {
            return this.getParentBranchId(node.data.parentNodeId);
        }
    }

    /**
     * Returns the root branch id
     */
    public getRootBranchId(branchId: string): string | undefined {
        const node = this.getRenderedNode(branchId);

        if (!node) {
            return;
        }

        if (this.isBaseTestable(branchId)) {
            return node.data.sourceNodeId;
        } else if (this.isTestablePartial(branchId)) {
            return this.getRootBranchId(node.data.parentNodeId);
        } else if (this.isStepAnchor(branchId)) {
            return this.getRootBranchId(node.data.sourceNodeId || '');
        }
    }

    /**
     * Branch must exist though we can't create it for you
     */
    private ensureIsAnOffersBranch(branchId: string) {
        const branch  = this.getRenderedNode(branchId);

        if (!branch) {
            return;
        }

        branch.type = 'branch-offers';
        branch.data = {
            isInsideBranch: false, // here so that it may be overridden if it is inside a branch
            ...branch.data,
            renderedNodeType: 'branch-offers', // here because this needs to be set to branch-offers
        }
    }

    private getBranchRelations(targetBranchId: string): NodesStore['branchRelations'][number] | undefined {
        const node = this.getRenderedNode(targetBranchId);

        if (this.isBaseTestable(targetBranchId)) {
            // if it's a tiered base node, we need to get the root testable for this branch
            //targetBranchId = node.data?.sourceNodeId?.replace(/->testable$/, '');
        }
        return this.state.branchRelations.find(branch => branch.branch === targetBranchId);
    }

    updateOffer(offer: OfferData) {
        const offerToUpdate = this.getOffer(offer.id)

        if (!offer) {
            return;
        }

        const offerIndex = this.state.offers.findIndex(({id}) => id === offer.id);

        if (offerIndex === -1) {
            return;
        }

        this.state.offers[offerIndex] = merge(this.state.offers[offerIndex], offer);
    }

    removeOffer(id: string) {
        this.perform('removeOffer', {data: {id}})
    }

    private performRemoveOffer({id}: {id: string}) {
        const offer = this.state.offers.find(offer => offer.id === id);

        if (!offer) {
            return;
        }

        // remove from offers array
        this.state.offers.splice(this.state.offers.indexOf(offer), 1);

        // now we need to remove the offer from the branch
        const parentBranchNode = this.getParentBranch(offer.id);

        this.removeBranchChildren(parentBranchNode?.id, [offer.id]);

    }

    /**
     * ALL This is a duplicate of Testables, it needs be refactored when we have time
     */
    perform<Data>(action: 'removeOffer' | 'removeOffersBranch' | 'removeNode' | 'removeBranchChildren' | 'convertBranch', {data}: {data: Data}) {
        this[`perform${action.charAt(0).toUpperCase() + action.slice(1)}`](data)
        this.maybeCallActions();
    }

    /**
     * Will hold the cleanup actions until the transaction is finished.
     * When you run a mutable function, a "cleanup" action will be performed after that.
     *
     * For example, if you remove a node, an action will be scheduled to check if its parent has no children and if not,
     * it will be removed too because we don't want to have empty nodes.
     *
     * So if you remove a node inside a transaction, the cleanup action will not be executed until the transaction is finished.
     *
     * This allows us, for example, to remove a node and then add a new one to its parent.
     *
     * If you remove a node outside a transaction and that node was the only child of its parent's node, the parent would be removed, and you wouldn't be able to add another node to that parent
     *
     * eg:
     *
     * the parent of testable is context-8758 and it has no other children
     * this.remove('testable')
     * this.addTestables([newTestable], 'context-8758') // this would fail because context-8758 would have been removed by now
     *
     * so you'd have to perform those operations inside a transaction
     *
     * this.transaction(() => {
     *   this.remove('testable')
     *   // the parent isn't removed yet because we are inside a transaction
     *   this.addTestables([newTestable], 'context-8758')
     *   // end of transaction, cleanup actions are executed but parent is still there because we added a new testable to it
     * })
     */
    transaction(callable: Function) {
        this.isInsideATransaction = true
        const result = callable()
        this.isInsideATransaction = false

        this.maybeCallActions()

        return result;
    }
    private maybeCallActions = () => {
        if (this.isInsideATransaction) {
            return
        }
        for (const [index, action] of this.afterPerformActionsQueue.entries()) {
            action()
        }
        this.afterPerformActionsQueue = []
    }

    public getParentBranch<RenderedNodeType = RenderedNode>(id: string) : RenderedNodeType | undefined {
        const branchRelations = this.state.branchRelations.find(branch => branch.children.includes(id));

        if (!branchRelations) {
            return;
        }

        return this.state.renderedNodes.find(node => node.id === branchRelations.branch) as RenderedNodeType;
    }

    private removeBranchChildren(parentBranchNodeId: string | undefined, childrenIDS: string[]) {
        this.perform('removeBranchChildren', {data: {parentBranchNodeId, childrenIDS}})
    }

    private performRemoveBranchChildren({parentBranchNodeId, childrenIDS}: {parentBranchNodeId: string | undefined, childrenIDS: string[]}) {
        if (!parentBranchNodeId) {
            return;
        }
        const branchRelations = this.getBranchRelations(parentBranchNodeId);

        if (!branchRelations) {
            return;
        }

        childrenIDS.forEach(childId => {
            const childIndex = branchRelations.children.indexOf(childId);

            if (childIndex !== -1) {
                branchRelations.children.splice(childIndex, 1);
            }
        })

        // schedule the cleanup action
        this.afterPerformActionsQueue.push(() => {
            this.convertOffersBranchWithNoChildrenToSelectActionBranch(branchRelations.branch);
        })
    }

    private addSelectActionBranchToSingleRootNodes() {
        const singleRootNodes = this.getRootNodes().filter(node => {
            return !this.getRelatedNode(node.id);
        })

        singleRootNodes.forEach(node => {
            this.addSelectActionBranch(node.id);
        })
    }

    private convertOffersBranchWithNoChildrenToSelectActionBranch(id: any) {
        const branch = this.getRenderedNode(id);

        if (branch?.type !== 'branch-offers') {
            return;

        }
        const branchChildren = this.getBranchChildren(id)

        if (branchChildren.length > 0) {
            return;
        }

        this.convertBranchTo('select-action-branch', id);
    }

    private convertBranchTo(type: keyof typeof nodeTypes, id: any) {
        this.perform('convertBranch', {data: {type, id}})
    }

    private performConvertBranch({type, id}: {type: keyof typeof nodeTypes, id: any}) {
        const branch = this.getRenderedNode(id);

        if (!branch) {
            return;
        }

        if (type === 'select-action-branch') {
            branch.type = type;
            branch.data = {
                ...branch.data,
                renderedNodeType: 'action',
            }
        } else if (type === 'tiered-base-node') {
            branch.type = type;
            branch.data = {
                renderedNodeType: 'branch-first-passing-node',
                targetNodeId: `${branch.id}->children->1->subchildren->1.step-anchor`,
            }
        }

        // here's where we may call the cleanup functions if needed for specific types
    }

    getOffer(id: string) : OfferData | undefined {
        return this.state.offers.find(offer => offer.id === id);
    }

    createTieredBranch({targetId, type, baseTestable, testablePartial, offers}: CreateTierBranchOptions): RenderedNode['id'] {
        const targetBranchToReplace = this.getRenderedNode(targetId);

        if (!targetBranchToReplace) {
            throw new Error('Branch not found when trying to create a tier branch');
        }

        //let's check this before we remove the node
        const isNested = this.isInsideBranch(targetId);
        const parentNode = this.getParentBranch(isNested && false? targetBranchToReplace.data.sourceNodeId! : targetId)!;


        return this.transaction(() => {
            //before we add the tier, let's add the base testable
            const newTestableId = this.testables.addTestables([baseTestable], undefined, false)[0]

            if (!newTestableId) {
                throw new Error('Could not create base Testable branch');
            }

            console.log('r the target?', targetId)
            // let's first remove that branch
            this.removeRenderedNode(targetId);
            // let's create the new branch
            const position = 1;
            const {baseTestableBranchId, testablePartialId, tierFinalBranchId} = this.generateTierBranchIds(
                position,
                isNested ? parentNode.id : undefined,
                isNested
            )

            if (isNested) {
                const targetSubChild = this.getRenderedNode(targetBranchToReplace.data.sourceNodeId!)!;
                targetSubChild.data = {
                    ...targetSubChild.data,
                    renderedNodeType: 'branch-first-passing-node',
                    targetNodeId: `${baseTestableBranchId}.step-anchor`,
                    sourceAnchor: {
                        id: 'bottom',
                        position: Position.Bottom
                    }
                }
            }
            // this is the main branch, this replaces the removed branch
            this.state.renderedNodes.push({
                id: baseTestableBranchId,
                type: 'tiered-base-node',
                // @ts-ignore
                data: pickBy({
                    type,
                    renderedNodeType: 'branch-first-passing-node',
                    sourceNodeId: isNested? `${baseTestableBranchId}.step-anchor` : targetBranchToReplace.data.sourceNodeId,
                    targetNodeId: `${testablePartialId}.step-anchor`,
                    testableID: newTestableId,
                    isInsideBranch: isNested || undefined,
                    parentNodeId: isNested ? parentNode.id : undefined,
                } as WithTargetNodeId, (v) => v !== undefined),
                position: {x: 0, y: 0},
            })

            /*this.state.branchRelations.push({
                branch: targetId.replace(/->branch$/, ''),
                children: [testablePartialId],
                childrenType: 'renderedNodes'
            })*/

            this.setCouponsPlusNodeType(baseTestableBranchId, 'tiered')

            this.addChildrenRelations(parentNode.id, [baseTestableBranchId])
            // step anchor for the base testable
            this.addStepAnchor(baseTestableBranchId, {isFirst: true, type: 'tiered-base'})

            this.addTier(baseTestableBranchId, testablePartial, offers, position)

            return baseTestableBranchId;
        });
    }

    addTier(targetBaseTestableBranch: string, testablePartial: CreateTierBranchOptions['testablePartial'], offers: CreateTierBranchOptions['offers'] = [], position?: number) : RenderedNode['id']  | undefined {
        if (!position) {
            // we'll add it to the end, so we need to rind out which is the last tier in this branch

            position = this.getBranchChildren(targetBaseTestableBranch).length + 1
        }
        // instead of creating a new branch, we are going to add a new tier to the existing one
        const {testablePartialId, tierFinalBranchId} = this.generateTierBranchIds(position, targetBaseTestableBranch)
        const baseTestableBranchId = targetBaseTestableBranch

        if (!this.getRenderedNode(baseTestableBranchId)) {
            throw new Error('Could not create base Testable branch');
        }
        const newTestableId = this.getRenderedNode(baseTestableBranchId)?.data?.testableID as string

        // now the partial
        const newTestablePartialId = this.testables.addTestablePartials([testablePartial], newTestableId)[0]

        // add the testable partial
        this.state.renderedNodes.push({
            id: testablePartialId,
            type: 'tiered-subchild-node',
            // @ts-ignore
            data: {
                renderedNodeType: 'branch-first-passing-node',
                targetNodeId: tierFinalBranchId,
                parentNodeId: baseTestableBranchId,
                testablePartialId: newTestablePartialId,
            } as WithTargetNodeId & WithParentNodeId,
            position: {x: 0, y: 0},
        })

        // this is the relationship between the tier branch and its children (subchildren/testablePartials)
        this.addChildrenRelations(baseTestableBranchId, [testablePartialId])

        // now the step anchor for the testable partial
        this.addStepAnchor(testablePartialId, {isFirst: position === 1, type: 'tier-row'})

        // then add the final tier branch (offers, etc.)
        if (offers?.length) {
            this.addOffersBranch(tierFinalBranchId, testablePartialId, offers)
            // we have to add the relation between the subchild and its branch (eg 865656->children->1->subchildren->1 as parent and 865656->children->1->subchildren->1->branch as child)
            this.addChildrenRelations(testablePartialId, [tierFinalBranchId])
        } else {
            // we have to add a select actions branch if we don't have offers
            this.addSelectActionBranch(testablePartialId)
        }

        this.updateAddTierButton(baseTestableBranchId)
        // you know what? instead of adding the edges manually, why don't we check for all rendered nodes that don't have edges? it might be a little slower, but it will be more reliable
        this.connectRenderedNodes()
        return testablePartialId;
    }

    cloneTier(targetTierId: string) : RenderedNode['id'] | undefined {
        const targetTier = this.getRenderedNode(targetTierId);

        if (!targetTier) {
            return;
        }
        const pastTestablePartialCopy = cloneDeep(this.testables.getNode(targetTier.data.testablePartialId) as TestablePartial);

        const tieredType = this.getTieredType(targetTier.id);
        let newTestablePartialOptions: object = {}

        if (tieredType === TierType.Numeric) {
            // here we'll change the options (eg: increase the number) but first let's clone the previous tier
            newTestablePartialOptions = increase(pastTestablePartialCopy.options);
        }

        return this.addTier(
            this.getParentBranch(targetTierId)!.id,
            // the options should be the same as the previous one so let's use the testable partial of the target tier
            {
                options: newTestablePartialOptions
            },
            // ok so now for the final branch of the tier, if its a simple branch, and
            //      a - has offers, get them
            //      b - its a select actuon, leave this blank
            //BUT if it has nested tiers, we'd leave this blank and then we have to clone those tiers too, but thats for later
            // perhaps omit the ids?
            this.isOffersBranch(targetTier.data.targetNodeId!) ? cloneDeep(this.getBranchChildren(targetTier.data.targetNodeId!).map(offer => {
                // let's not delete the id of the original offer!
                offer = cloneDeep(offer)
                // remove offer id
                if (offer.id) {
                    // @ts-ignore
                    delete offer.id
                }
                return offer;
            })) : [],
        )
    }
    addOffersBranch(id: string | undefined, sourceNodeId: string, offers: Omit<OfferData, 'id'>[]) {
        id = id || `branch-${generate()}`

        this.state.renderedNodes.push({
            id,
            type: 'branch-offers',
            data: {
                renderedNodeType: 'branch-offers',
                isInsideBranch: true, // this is VERY important!
                sourceNodeId,
                xSpacing: 18 * 4 // This is going to be calculated based on the width of the 'pass items' label IF the source node is a testable
            } as OffersBranchData & WithXSpacing,
            //                position: {x: 240, y: 290},
            position: {x: 0, y: 0},
        })

        this.addOffers(offers, id);

        this.setCouponsPlusNodeType(id, 'offers')
    }

    generateTierBranchIds(position: number, baseTestableBranchId?: string, nested: boolean = false): {
        baseTestableBranchId: string,
        testablePartialId: string,
        tierFinalBranchId: string
    } {
        if (!baseTestableBranchId) {
            baseTestableBranchId = `${this.createId('branch')}->children->${position}`
        } else if (baseTestableBranchId && nested) {
            baseTestableBranchId = `${baseTestableBranchId}->children->${position}`
        }
        const testablePartialId = `${baseTestableBranchId}->subchildren->${position}`
        const tierFinalBranchId = `${testablePartialId}->branch`

        return {
            baseTestableBranchId,
            testablePartialId,
            tierFinalBranchId,
        };
    }

    private addStepAnchor(targetBranchId: string, options: Required<Pick<StepAnchorData, 'isFirst' | 'type'>> & Partial<Omit<StepAnchorData, 'isFirst' | 'type'>>) {
        let {
            id,
            sourceNodeId,
            targetNodeId,
            isFirst,
            type,
            label,
            direction,
            y
        } = this.getStepAnchorObjectData(targetBranchId, options);

        this.state.renderedNodes.push({
            id,
            type: 'step-anchor',
            data: pickBy({
                renderedNodeType: 'step-anchor',
                isFirst,
                sourceNodeId,
                targetNodeId,
                type,
                /** tier-row only (subchild) **/
                label,
                direction,
                y,
                /*end*/
                xSpacing: isFirst && type === 'tiered-base'? 8 * 4 : undefined
            } as StepAnchorData & WithXSpacing, (v) => v !== undefined),
            position: {x: 0, y: 0},
        })
    }

    private getStepAnchorObjectData(targetBranchId: string, options: Required<Pick<StepAnchorData, 'isFirst' | 'type'>> & Partial<Omit<StepAnchorData, 'isFirst' | 'type'>>) {
        const targetNode = this.getRenderedNode(targetBranchId);

        let sourceNodeId = targetNode?.data?.sourceNodeId
        let targetNodeId = targetBranchId
        let {isFirst, type} = options
        let label: string | undefined, direction: StepAnchorData['direction'] | undefined, y: StepAnchorData['y']

        if (type === 'tier-row') {
            sourceNodeId = targetNode?.data.parentNodeId
            label = String.fromCharCode(96 + this.getBranchChildIndex(targetBranchId) + 1).toUpperCase();
            direction = 'horizontal'
            y = 'middle'
        }

        if (!isFirst) {
            // the source is the anchor of the previous tier
            const currentTestablePartial = this.getRenderedNode(targetBranchId);
            const allTiers = this.getBranchChildren(currentTestablePartial?.data.parentNodeId || '');
            const currentIndex = this.getBranchChildIndex(targetBranchId)
            const previousTier = allTiers[currentIndex - 1];

            sourceNodeId = `${previousTier.id}.step-anchor`
        }

        if (this.isInsideBranch(targetBranchId)) {
            sourceNodeId = this.getRenderedNode(targetBranchId)?.data.parentNodeId
        }

        return {
            id: `${targetNodeId}.step-anchor`,
            sourceNodeId,
            targetNodeId,
            isFirst,
            type,
            label,
            direction,
            y
        };
    }

    private createId(type: 'branch') {
        return `${type}-${generate()}`;
    }

    public connectRenderedNodes() {
        // check all the nodes that have a source/target and that don't have edges
        // only connect them if both source and target exist
        for (const renderedNode of this.state.renderedNodes) {
            if (renderedNode.type === 'step-anchor') {
                const edge = this.getEdgeById(renderedNode.id);

                if (edge) {
                    continue;
                }

                // if its the base testable
                const isBase = renderedNode.data?.type === 'tiered-base' && renderedNode.data?.isFirst;
                if (isBase) {
                    if (this.isStepAnchor(renderedNode.id) && this.isInsideBranch(renderedNode.data.targetNodeId!)) {
                        // its nested so let's connect the step anchor of the base testable to the partial testable of the parent branch
                        this.state.edges.push({
                            id: renderedNode.id,
                            targetHandle: 'left',
                            sourceHandle: 'bottom',
                            source: renderedNode.data.sourceNodeId || '',
                            target: `${renderedNode.data.targetNodeId}.step-anchor`,
                            type: 'connecting-line',
                        })
                    } else {
                        // if it's the base testable, we need to add a left and right handle
                        // the base needs to left and right handles and its target is the target testable
                        this.state.edges.push({
                            id: renderedNode.id,
                            targetHandle: 'left',
                            sourceHandle: 'right',
                            source: renderedNode.id,
                            target: renderedNode.data.targetNodeId!,
                            type: 'connecting-line',
                        })
                    }
                } else {
                    // this is a type: 'tier-row',
                    if (renderedNode.data?.isFirst) {
                        this.state.edges.push(
                            {
                                id: renderedNode.id,
                                targetHandle: 'left',
                                sourceHandle: 'right',
                                source: `${renderedNode.data.sourceNodeId}`,
                                target: `${renderedNode.data.targetNodeId}`,
                                type: 'connecting-line',
                            }
                        )
                    } else {
                        this.state.edges.push(
                            {
                                id: renderedNode.id,
                                targetHandle: 'top',
                                sourceHandle: 'bottom',
                                source: renderedNode.data.sourceNodeId!,
                                target: renderedNode.id,
                                type: 'connecting-line',
                            },
                        )
                    }
                }
                continue;
            }

            if (renderedNode.type === 'tiered-base-node') {
                if (this.getEdgeById(renderedNode.id)) {
                    continue
                }
                if (this.isInsideBranch(renderedNode.id)) {
                    //nested base testable, we need to connect it to its step anchor
                    this.state.edges.push({
                        id: renderedNode.id,
                        targetHandle: 'left',
                        sourceHandle: 'right',
                        source: `${renderedNode.id}.step-anchor`,
                        target: renderedNode.id,
                        type: 'connecting-line',
                    })
                } else {
                    this.state.edges.push({
                        id: renderedNode.id,
                        targetHandle: 'left',
                        sourceHandle: 'right',
                        source: renderedNode.id,
                        target: `${this.getStepAnchorWithSourceId(renderedNode.id)?.data.targetNodeId}.step-anchor`,
                        type: 'connecting-line',
                    })
                }
                continue
            }
            if (renderedNode.type === 'tiered-subchild-node' && !this.getEdgeById(renderedNode.id)) {
                const isFirstTier = this.isFirstTier(renderedNode.id)

                if (isFirstTier) {
                    // the first tier dont need connection as its already redundant
                    continue
                }

                this.state.edges.push({
                    id: renderedNode.id,
                    targetHandle: 'left',
                    sourceHandle: 'right',
                    source: `${renderedNode.id}.step-anchor`,
                    target: `${renderedNode.id}`,
                    type: 'connecting-line',
                })

                continue;
            }

            if (renderedNode.type === 'branch-add-node-button' && !this.getEdgeById(renderedNode.id)) {
                this.state.edges.push({
                    id: renderedNode.id,
                    targetHandle: 'top',
                    sourceHandle: 'bottom',
                    source: renderedNode.data.lastAnchor,
                    target: renderedNode.id,
                    type: 'connecting-line',
                })
            }

            if (renderedNode.data.isInsideBranch && !this.getEdgeById(renderedNode.id)) {
                // if it's inside a branch, we need to connect it to the parent branch
                this.state.edges.push({
                    id: renderedNode.id,
                    targetHandle: 'left',
                    sourceHandle: 'right',
                    source: renderedNode.data.sourceNodeId!,
                    target: renderedNode.id,
                    type: 'connecting-line',
                })

                continue;
            }

            const children = this.getBranchChildren<RenderedNode>(renderedNode.id);

            if (children.length > 0 && !this.getEdgeById(renderedNode.id)) {
                // if it has children, we need to connect it to the first child
                const firstChild = children[0];

                // if the child is a tiered branch, the target shall be the step anchor
                this.state.edges.push({
                    id: renderedNode.id,
                    source: renderedNode.id,
                    target: firstChild.type === 'tiered-base-node'? `${firstChild.id}.step-anchor` :  firstChild.id,
                    type: 'connecting-line',
                })

                continue;

            }
            if (renderedNode.data?.sourceNodeId || renderedNode.data?.targetNodeId) {
                let source = this.getRenderedNode(renderedNode.data?.sourceNodeId || '');
                let target = this.getRenderedNode(renderedNode.data?.targetNodeId || '');

                let sourceID: string, targetID: string;
                // has a source but not a target
                if (source && !target) {
                    sourceID = source.id;
                    targetID = renderedNode.id;
                } else if (!source && target) {
                    // has a target but not a source
                    sourceID = renderedNode.id;
                    targetID = target.id;
                } else if (source && target) {
                    // has both a source and a target
                    sourceID = source?.id;
                    targetID = target?.id;
                } else {
                    // has neither a source nor a target
                    continue;
                }

                if (sourceID && targetID) {
                    const edge = this.state.edges.find(edge => edge.source === sourceID && edge.target === targetID);
                    if (!edge) {
                        this.state.edges.push({
                            id: sourceID,
                            source: sourceID,
                            target: targetID,
                            type: 'connecting-line',
                        });
                    }
                }
            }
        }
    }

    public isFirstTier(renderedNodeId: string) {
        return this.getBranchChildIndex(renderedNodeId) === 0;
    }

    private getEdgeById(renderedNodeId: string) {
        return this.state.edges.find(edge => edge.id === renderedNodeId);
    }

    getEdgeBySource(sourceId: string) {
        return this.state.edges.find(edge => edge.source === sourceId);
    }

    public getEdgesThatConnectTo(nodeId: string) {
        return this.state.edges.filter(edge => edge.source === nodeId || edge.target === nodeId);
    }

    private getBranchChildIndex(branchId: string): number {
        let node = this.getRenderedNode(branchId);
        if (typeof node === 'undefined') {
            return -1;
        }

        if (this.isTestablePartial(node.id)) {
            node = this.getRenderedNode(node.data.parentNodeId)
        }

        if (typeof node === 'undefined') {
            return -1;
        }

        const index =  this.getBranchRelations(node.id)?.children.findIndex(child => {
            return child === branchId
        });

        if (typeof index === 'undefined' || index === -1) {
            return -1;
        }

        return index;
    }

    getLastBranchChild(id: string): RenderedNode | undefined {
        return this.getBranchChildren(id).slice(-1)[0];
    }

    private addChildrenRelations(parentId: string, childIDs: string[]) {
        if (this.isBaseTestable(parentId)) {
            //parentId = this.getRootBranchId(parentId) || parentId;
        }

        const branchRelations = this.getBranchRelations(parentId);

        if (branchRelations) {
            branchRelations.children.push(...childIDs);
        } else {
            this.state.branchRelations.push({
                branch: parentId,
                children: childIDs,
                childrenType: 'renderedNodes'
            })
        }
    }

    private getStepAnchorWithSourceId(id: string) {
        return this.state.renderedNodes.find(node => {
            return node.type === 'step-anchor' && node.data.sourceNodeId === id
        });
    }

    isBaseTestable(branchId: string) {
        return this.getRenderedNode(branchId)?.type === 'tiered-base-node';
    }

    isTestablePartial(branchOrId: string | RenderedNode) {
        let branch: string | RenderedNode | undefined

        if (typeof branchOrId === 'string') {
           branch = this.getRenderedNode(branchOrId)
        } else {
            branch = branchOrId
        }

        return branch?.type === 'tiered-subchild-node';
    }

    private updateAddTierButton(baseTestableBranchId: string) {
        console.log('update button', this.isBaseTestable(baseTestableBranchId), baseTestableBranchId)
        if (!this.isBaseTestable(baseTestableBranchId)) {
            return;
        }
        // add the button if it doesnt exist
        let buttonNode = this.getAddTierButtonNode(baseTestableBranchId);

        if (!buttonNode) {
            buttonNode = {
                id: `${baseTestableBranchId}->subchildren->buttons.add`,
                type: 'branch-add-node-button',
                data: {
                    renderedNodeType: 'branch-add-node-button',
                    parentNodeId: baseTestableBranchId,
                    lastAnchor: ''// IT'S BEING ADDED BELLOW!  //'tiered-example->children->1->subchildren->4.step-anchor'
                },
                position: {x: 0, y: 0},
            }

            this.state.renderedNodes.push(buttonNode)
        }

        // let's make sure the last anchor is the same as the last tier
        const lastTier = this.getBranchChildren(baseTestableBranchId).slice(-1)[0]
        const lastTierAnchorId = `${lastTier?.id}.step-anchor`

        const prevLastAnchor = buttonNode.data.lastAnchor;
        if (prevLastAnchor !== lastTierAnchorId) {
            buttonNode.data.lastAnchor = lastTierAnchorId
            // update the edges to!
            const oldEdge = this.getEdgeBySource(prevLastAnchor);

            if (oldEdge) {
                oldEdge.source = lastTierAnchorId;
            }
        }
    }

    public getAddTierButtonNode(baseTestableBranchId: string) {
        return this.state.renderedNodes.find(node => {
            return this.isAddTierButton(node.id) && node.data?.parentNodeId === baseTestableBranchId
        });
    }

    private isStepAnchor(id: string) {
        const node = this.getRenderedNode(id);

        return node?.type === 'step-anchor'
    }

    private setCouponsPlusNodeType(id: string, type: NodesStore['couponsPlusNodeTypes'][number]['branchType']) {
        let rootId: string

        if (this.isBaseTestable(id)) {
            rootId = this.getRootBranchId(id)!;
        } else {
            rootId = id.endsWith('->branch')? id.replace(/->branch$/, '') : id;
        }

        if (!rootId) {
            return;
        }

        const couponsPlusNodeType = this.state.couponsPlusNodeTypes.find(({id}) => id === rootId);

        if (couponsPlusNodeType) {
            couponsPlusNodeType.branchType = type;
        } else {
            this.state.couponsPlusNodeTypes.push({
                id: rootId,
                branchType: type,
            })
        }
    }

    private removeBranchRelations(id: string) {
        if (this.isTestablePartial(id)) {
            const branchRelationsIndex = this.state.branchRelations.findIndex(
                branch => branch.children.includes(id)
            );
            const branchRelations = this.state.branchRelations[branchRelationsIndex]

            if (!branchRelations) {
                return;
            }

            if (branchRelations.children.length > 1) {
                // we have more than one subchild, so just remove the current one
                const childIndex = branchRelations.children.indexOf(id);
                if (childIndex !== -1) {
                    branchRelations.children.splice(childIndex, 1);
                }
            } else {
                // only one subchild, remove the whole thing
                this.state.branchRelations.splice(branchRelationsIndex, 1);
            }
        } else {
            // its an offers brach
            const branchRelationsIndex = this.state.branchRelations.findIndex(branch => branch.branch === id);
            if (branchRelationsIndex !== -1) {
                this.state.branchRelations.splice(branchRelationsIndex, 1);
            }
        }
    }

    private isOffersBranch(id: string) {
        return this.getRenderedNode(id)?.type === 'branch-offers';
    }

    private removeOffersBranch(id: string) {
        this.perform('removeOffersBranch', {data: {id}})
    }

    private performRemoveOffersBranch({id}: {id: string}) {
        const branch = this.getRenderedNode(id);

        if (!branch) {
            return;
        }

        const offers = this.getBranchChildren<OfferData>(id);

        offers.forEach(offer => {
            this.removeOffer(offer.id);
        })

        // remove the branch relations
        this.removeBranchRelations(id);

        // remove it from the coupons plus node types
        this.removeCouponsPlusNodeType(id);

        // maybe replace it with a select action branch IF the rendered node still exists
        this.afterPerformActionsQueue.push(() => {
            // to do (as said above)
        })
    }

    public getRootNodes(): RenderedNode[] {
        return this.state.renderedNodes.filter(node => {
            return node.type === 'tree-node' && node.data?.renderedNodeType === 'root';
        })
    }

    /**
     * For Testable-Branch relations
     *
     * if the id is a testable, the return value would be the branch associated with it
     * if the if is the branch, the return value would be the testable
     */
    public getRelatedNode(id: string) : RenderedNode | undefined {
        if (this.isTreeNode(id)) {
            // get the branch
            return this.getBranchChildren(id)[0];
        } else {
            // its the branch so get the testable
            return this.getParentBranch(id);
        }
    }

    private isTreeNode(id: string) {
        const node = this.getRenderedNode(id);

        return node?.type === 'tree-node'
    }

    private removeCouponsPlusNodeType(id: string) {
        id = this.getCouponsPlusTypeId(id)

        const couponsPlusNodeTypeIndex = this.state.couponsPlusNodeTypes.findIndex(({id: couponsPlusId}) => couponsPlusId === id);

        if (couponsPlusNodeTypeIndex !== -1) {
            this.state.couponsPlusNodeTypes.splice(couponsPlusNodeTypeIndex, 1);
        }
    }

    private getCouponsPlusTypeId(id: string) : string {
        if (this.isInsideBranch(id) && this.isBranch(id)) {
            return id.replace(/->branch$/, '');
        }

        if (this.isRootTreeNode(id)) {
            return id.replace(/->testable$/, '');
        }

        return id;
    }

    public isInsideBranch(id: string) {
        return this.getRenderedNode(id)?.data?.isInsideBranch;
    }

    private isBranch(id: string) {
        return (['select-action-branch', 'branch-offers', 'tiered-base-node'] as (keyof typeof nodeTypes)[]).includes(
            this.getRenderedNode(id)?.type!
        );
    }

    private isRootTreeNode(id: string) {
        return this.isTreeNode(id) && this.getRenderedNode(id)?.data?.renderedNodeType === 'root';
    }

    static createFromState(state: TreeState & TestablesState) : Tree {
        return new Tree(state, new Testables(state));
    }

    private isAddTierButton(renderedNodeId: string) : boolean {
        const node = this.getRenderedNode(renderedNodeId);

        if (!node) {
            return false;
        }

        return node.type === 'branch-add-node-button';
    }

    public getTieredType(id: string) :  TierType {
        return this.getParentBranch(id)?.data.type as TierType;
    }

    private updateTieredBranchRelationships(tieredBaseId: string) {
        if (!this.isBaseTestable(tieredBaseId)) {
            return
        }

        const tieredBaseNode = this.getRenderedNode(tieredBaseId);

        if (!tieredBaseNode) {
            return;
        }
        // so we have to update the tiered base data to point to the first available subchile
        const subChilds = this.getBranchChildren(tieredBaseId);

        const firstSubChild = subChilds[0];

        if (!firstSubChild) {
            // there are no subchildren so we have to remove this branch </3
            this.removeRenderedNode(tieredBaseId);
            return;
        }

        tieredBaseNode.data.targetNodeId = `${firstSubChild.id}.step-anchor`;

        // now we have to update the step anchors of all the subchildren
        subChilds.forEach((subChild, index) => {
            const stepAnchor = this.getStepAnchorForNode(subChild)!
            //testablePartialId, {isFirst: position === 1, type: 'tier-row'}
            const stepAnchorData = this.getStepAnchorObjectData(
                subChild.id,
                {
                    isFirst: index === 0,
                    type: 'tier-row'
                }
            )

            for (const key in stepAnchorData) {
                if (key === 'id') {
                    continue;
                }
                stepAnchor.data[key] = stepAnchorData[key];
            }
        })
    }

    private getStepAnchorForNode(subChild: RenderedNode) {
        return this.getRenderedNode(`${subChild.id}.step-anchor`);
    }
}

export type CreateTierBranchOptions = {
    // a select action branch or a testable branch for nested tier branches
    targetId: string,
    // this is the type of the value of the testables
    // numeric, path-fixed, path-variable, path-boolean
    // currently only numeric and path-fixed are supported
    type: TierType /*| 'path-variable' | 'path-boolean'*/,
    // we have to pass the entire object bc some types may have editable fields (eg: location, where to look? ip, billing address, etc, this can be configurable, only this, the actual values are configurable on each tier)
    baseTestable: Omit<TestableData, 'id'>,
    // no base if bc we're going to use the one from the passed base testable
    testablePartial: Omit<TestablePartial, 'id'>,
    // this one is optional, if not passed, a select action branch will be added
    offers?: Omit<OfferData, 'id'>[],
};
