import { isNil } from '@activepieces/core-utils'
import { ActivepiecesError, ErrorCode } from '@activepieces/core-utils'
import { BranchCondition, BranchExecutionType, emptyCondition, FlowAction, FlowActionType } from '../actions/action'
import { FlowVersion } from '../flow-version'
import { FlowTrigger, FlowTriggerType } from '../triggers/trigger'


export const AI_PIECE_NAME = '@activepieces/piece-ai'

export type Step = FlowAction | FlowTrigger
type StepWithIndex = Step & {
    dfsIndex: number
}

function isAction(type: FlowActionType | FlowTriggerType | undefined): type is FlowActionType {
    return Object.entries(FlowActionType).some(([, value]) => value === type)
}

function isStepAction(step: Step): step is FlowAction {
    return step.type === FlowActionType.CODE
        || step.type === FlowActionType.PIECE
        || step.type === FlowActionType.LOOP_ON_ITEMS
        || step.type === FlowActionType.ROUTER
}

function isTrigger(type: FlowActionType | FlowTriggerType | undefined): type is FlowTriggerType {
    return Object.entries(FlowTriggerType).some(([, value]) => value === type)
}

function getActionOrThrow(name: string, flowRoot: Step): FlowAction {
    const step = getStepOrThrow(name, flowRoot)
    if (!isAction(step.type)) {
        throw new ActivepiecesError({
            code: ErrorCode.ENTITY_NOT_FOUND,
            params: {
                entityType: 'step',
                entityId: name,
                message: 'Step is not an action',
            },
        })
    }
    return step as FlowAction
}

function getTriggerOrThrow(name: string, flowRoot: Step): FlowTrigger {
    const step = getStepOrThrow(name, flowRoot)
    if (!isTrigger(step.type)) {
        throw new ActivepiecesError({
            code: ErrorCode.ENTITY_NOT_FOUND,
            params: {
                entityType: 'step',
                entityId: name,
                message: 'Step is not a trigger',
            },
        })
    }
    return step as FlowTrigger
}

function getStep(name: string, flowRoot: Step): Step | undefined {
    return getAllSteps(flowRoot).find((step) => step.name === name)
}

function getStepOrThrow(name: string, flowRoot: Step): Step {
    const step = getStep(name, flowRoot)
    if (isNil(step)) {
        throw new ActivepiecesError({
            code: ErrorCode.ENTITY_NOT_FOUND,
            params: {
                entityType: 'step',
                entityId: name,
                message: 'Step not found',
            },
        })
    }
    return step
}

function transferStep<T extends Step>(
    step: Step,
    transferFunction: (step: T) => T,
): Step {
    const updatedStep = transferFunction(step as T)
    switch (updatedStep.type) {
        case FlowActionType.LOOP_ON_ITEMS: {
            const { firstLoopAction } = updatedStep
            if (firstLoopAction) {
                updatedStep.firstLoopAction = transferStep(
                    firstLoopAction,
                    transferFunction,
                ) as FlowAction
            }
            break
        }
        case FlowActionType.ROUTER: {
            const { children } = updatedStep
            if (children) {
                updatedStep.children = children.map((child) =>
                    child ? (transferStep(child, transferFunction) as FlowAction) : null,
                )
            }
            break
        }
        default:
            break
    }

    if (updatedStep.type === FlowActionType.CODE || updatedStep.type === FlowActionType.PIECE) {
        const branches = updatedStep.continueOnFailureBranches
        if (branches?.onSuccess) {
            const transferred = transferStep(branches.onSuccess, transferFunction)
            if (isStepAction(transferred)) {
                branches.onSuccess = transferred
            }
        }
        if (branches?.onFailure) {
            const transferred = transferStep(branches.onFailure, transferFunction)
            if (isStepAction(transferred)) {
                branches.onFailure = transferred
            }
        }
    }

    if (updatedStep.nextAction) {
        updatedStep.nextAction = transferStep(
            updatedStep.nextAction,
            transferFunction,
        ) as FlowAction
    }

    return updatedStep
}


function transferFlow<T extends Step>(
    flowVersion: FlowVersion,
    transferFunction: (step: T) => T,
): FlowVersion {
    const clonedFlow = JSON.parse(JSON.stringify(flowVersion))
    clonedFlow.trigger = transferStep(
        clonedFlow.trigger,
        transferFunction,
    ) as FlowTrigger
    return clonedFlow
}

function getAllSteps(step: Step): Step[] {
    const steps: Step[] = []
    transferStep(step, (currentStep) => {
        steps.push(currentStep)
        return currentStep
    })
    return steps
}

function getStepNumber(trigger: FlowTrigger, stepName: string): number {
    return getAllSteps(trigger).findIndex((s) => s.name === stepName) + 1
}


const createBranch = (branchName: string, conditions: BranchCondition[][] | undefined) => {
    return {
        conditions: conditions ?? [[emptyCondition]],
        branchType: BranchExecutionType.CONDITION,
        branchName,
    }
}

function findPathToStep(trigger: FlowTrigger, targetStepName: string): StepWithIndex[] {
    const steps = flowStructureUtil.getAllSteps(trigger).map((step, dfsIndex) => ({
        ...step,
        dfsIndex,
    }))
    return steps
        .filter((step) => {
            const steps = flowStructureUtil.getAllSteps(step)
            return steps.some((s) => s.name === targetStepName)
        })
        .filter((step) => step.name !== targetStepName)
}


function getAllChildSteps(action: Step): Step[] {
    return getAllSteps({
        ...action,
        nextAction: undefined,
    })
}

function isChildOf(parent: Step, childStepName: string): boolean {
    return getAllChildSteps(parent).some((c) => c.name === childStepName && c.name !== parent.name)
}

const findUnusedNames = (source: FlowTrigger | string[], count = 1) => {
    const names = Array.isArray(source) ? source : flowStructureUtil.getAllSteps(source).map((f) => f.name)
    const unusedNames = []
    for (let i = 1; i <= count; i++) {
        const name = findUnusedName(names)
        unusedNames.push(name)
        names.push(name)
    }
    return unusedNames
}

const findUnusedName = (source: FlowTrigger | string[]) => {
    const names = Array.isArray(source) ? source : flowStructureUtil.getAllSteps(source).map((f) => f.name)
    let index = 1
    let name = 'step_1'
    while (names.includes(name)) {
        index++
        name = 'step_' + index
    }
    return name
}


function getAllNextActionsWithoutChildren(start: Step): Step[] {
    const actions: Step[] = []
    let currentAction = start.nextAction

    while (!isNil(currentAction)) {
        actions.push(currentAction)
        currentAction = currentAction.nextAction
    }

    return actions
}


function extractConnectionIdsFromAuth(auth: string): string[] {
    const match = auth.match(/{{connections\['([^']*(?:'\s*,\s*'[^']*)*)'\]}}/)
    if (!match || !match[1]) {
        return []
    }
    return match[1].split(/'\s*,\s*'/).map(id => id.trim())
}

function extractAgentIds(flowVersion: FlowVersion): string[] {
    const getExternalAgentId = (action: Step) => {
        if (isAgentPiece(action) && 'agentId' in action.settings.input) {
            return action.settings.input.agentId
        }
        return null
    }

    return flowStructureUtil.getAllSteps(flowVersion.trigger).map(step => getExternalAgentId(step)).filter(step => step !== null && step !== '')
}

function isAgentPiece(action: Step) {
    return (
        action.type === FlowActionType.PIECE && action.settings.pieceName === AI_PIECE_NAME
    )
}

function extractConnectionIds(flowVersion: FlowVersion): string[] {
    const triggerAuthIds = flowVersion.trigger.settings?.input?.auth
        ? extractConnectionIdsFromAuth(flowVersion.trigger.settings.input.auth)
        : []

    const stepAuthIds = flowStructureUtil
        .getAllSteps(flowVersion.trigger)
        .flatMap(step =>
            step.settings?.input?.auth
                ? extractConnectionIdsFromAuth(step.settings.input.auth)
                : [],
        )

    return Array.from(new Set([...triggerAuthIds, ...stepAuthIds]))
}

export const flowStructureUtil = {
    isTrigger,
    isAction,
    getAllSteps,
    getStepNumber,
    transferStep,
    transferFlow,
    getStepOrThrow,
    getActionOrThrow,
    getTriggerOrThrow,
    getStep,
    createBranch,
    findPathToStep,
    isChildOf,
    findUnusedName,
    findUnusedNames,
    getAllNextActionsWithoutChildren,
    getAllChildSteps,
    extractConnectionIds,
    isAgentPiece,
    extractAgentIds,
}