import { randomUUID } from "crypto"
import { prisma } from "@/core/prisma"
import { AppError } from "@/core/AppError"
import type {
    CreateProductConfiguratorInput,
    ProductConfiguratorDto,
    ProductConfiguratorProductUsageDto,
    ProductConfiguratorStepInput,
    ProductOptionChoiceInput,
    ProductOptionGroupDto,
    ProductOptionGroupInput,
    ProductOptionSelectionMode,
    ProductCustomizationMode,
    UpdateProductConfiguratorInput,
} from "ttm-shared"

const CONFIGURATOR_DETAIL_INCLUDE = {
    optionGroups: {
        orderBy: [{ displayOrder: "asc" as const }, { label: "asc" as const }],
        include: {
            choices: {
                orderBy: [{ displayOrder: "asc" as const }, { label: "asc" as const }],
            },
        },
    },
    steps: {
        orderBy: [{ displayOrder: "asc" as const }, { label: "asc" as const }],
        include: {
            entryGroup: {
                include: {
                    choices: {
                        orderBy: [{ displayOrder: "asc" as const }, { label: "asc" as const }],
                    },
                },
            },
        },
    },
    products: {
        orderBy: [{ active: "desc" as const }, { name: "asc" as const }],
        select: {
            id: true,
            name: true,
            price: true,
            active: true,
            customizationMode: true,
        },
    },
}

type NormalizedChoice = Omit<ProductOptionChoiceInput, "id"> & {
    id: string
    nextGroupId: string | null
    description: string | null
}

type NormalizedGroup = Omit<ProductOptionGroupInput, "id" | "choices"> & {
    id: string
    choices: NormalizedChoice[]
}

type NormalizedStep = Omit<ProductConfiguratorStepInput, "id"> & {
    id: string
    entryGroupId: string | null
}

function toIso(value: Date | string) {
    return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
}

function normalizeNullableText(value: string | null | undefined) {
    const trimmed = value?.trim() ?? ""
    return trimmed ? trimmed : null
}

function serializeProductUsage(product: {
    id: string
    name: string
    price: number
    active: boolean
    customizationMode: ProductCustomizationMode
}): ProductConfiguratorProductUsageDto {
    return {
        productId: product.id,
        productName: product.name,
        productPrice: product.price,
        active: product.active,
        customizationMode: product.customizationMode,
    }
}

function serializeOptionGroup(group: any): ProductOptionGroupDto {
    return {
        id: group.id,
        configuratorId: group.configuratorId,
        code: group.code,
        label: group.label,
        selectionMode: group.selectionMode,
        minChoices: group.minChoices,
        maxChoices: group.maxChoices,
        displayOrder: group.displayOrder,
        active: group.active,
        choices: (group.choices ?? []).map((choice: any) => ({
            id: choice.id,
            groupId: choice.groupId,
            code: choice.code,
            label: choice.label,
            description: choice.description,
            priceDelta: choice.priceDelta,
            displayOrder: choice.displayOrder,
            isDefault: choice.isDefault,
            nextGroupId: choice.nextGroupId,
            active: choice.active,
        })),
    }
}

function serializeConfigurator(configurator: any): ProductConfiguratorDto {
    const optionGroups: ProductOptionGroupDto[] = (configurator.optionGroups ?? []).map(serializeOptionGroup)
    const optionGroupsById = new Map(optionGroups.map((group) => [group.id, group]))

    return {
        id: configurator.id,
        shopId: configurator.shopId,
        name: configurator.name,
        description: configurator.description,
        active: configurator.active,
        productCount: configurator.products?.length ?? 0,
        createdAt: toIso(configurator.createdAt),
        updatedAt: toIso(configurator.updatedAt),
        optionGroups,
        steps: (configurator.steps ?? []).map((step: any) => ({
            id: step.id,
            configuratorId: step.configuratorId,
            code: step.code,
            label: step.label,
            displayOrder: step.displayOrder,
            minSelections: step.minSelections,
            maxSelections: step.maxSelections,
            repeatCount: step.repeatCount,
            entryGroupId: step.entryGroupId,
            entryGroup: step.entryGroupId ? (optionGroupsById.get(step.entryGroupId) ?? null) : null,
        })),
        products: (configurator.products ?? []).map(serializeProductUsage),
    }
}

function ensureUniqueCode(scopeLabel: string, entries: Array<{ code: string }>) {
    const seen = new Set<string>()
    for (const entry of entries) {
        const normalizedCode = entry.code.trim().toLowerCase()
        if (seen.has(normalizedCode)) {
            throw new AppError(`${scopeLabel} contains duplicate code \"${entry.code}\"`, 400)
        }
        seen.add(normalizedCode)
    }
}

function normalizeGroups(optionGroups: ProductOptionGroupInput[]): NormalizedGroup[] {
    const groups = optionGroups.map((group, index) => ({
        id: group.id ?? randomUUID(),
        code: group.code.trim(),
        label: group.label.trim(),
        selectionMode: group.selectionMode,
        minChoices: group.minChoices,
        maxChoices: group.maxChoices,
        displayOrder: group.displayOrder ?? index,
        active: group.active,
        choices: (group.choices ?? []).map((choice, choiceIndex) => ({
            id: choice.id ?? randomUUID(),
            code: choice.code.trim(),
            label: choice.label.trim(),
            description: normalizeNullableText(choice.description),
            priceDelta: choice.priceDelta,
            displayOrder: choice.displayOrder ?? choiceIndex,
            isDefault: choice.isDefault,
            nextGroupId: choice.nextGroupId ?? null,
            active: choice.active,
        })),
    }))

    ensureUniqueCode("Configurator option groups", groups)
    for (const group of groups) {
        ensureUniqueCode(`Option group ${group.code}`, group.choices)
    }

    return groups
}

function normalizeSteps(steps: ProductConfiguratorStepInput[]): NormalizedStep[] {
    const normalized = steps.map((step, index) => ({
        id: step.id ?? randomUUID(),
        code: step.code.trim(),
        label: step.label.trim(),
        displayOrder: step.displayOrder ?? index,
        minSelections: step.minSelections,
        maxSelections: step.maxSelections,
        repeatCount: step.repeatCount,
        entryGroupId: step.entryGroupId ?? null,
    }))

    ensureUniqueCode("Configurator steps", normalized)
    return normalized
}

function validateSelectionMode(group: NormalizedGroup) {
    if (group.minChoices > group.maxChoices) {
        throw new AppError(`Group ${group.code} has minChoices greater than maxChoices`, 400)
    }

    if (group.selectionMode === "SINGLE") {
        if (group.maxChoices !== 1) {
            throw new AppError(`Group ${group.code} in SINGLE mode must have maxChoices = 1`, 400)
        }
        if (group.minChoices > 1) {
            throw new AppError(`Group ${group.code} in SINGLE mode cannot require more than one choice`, 400)
        }
    }

    const defaultChoices = group.choices.filter((choice) => choice.isDefault && choice.active)
    if (group.selectionMode === "SINGLE" && defaultChoices.length > 1) {
        throw new AppError(`Group ${group.code} in SINGLE mode can only have one default choice`, 400)
    }
}

function detectCycles(groups: NormalizedGroup[]) {
    const graph = new Map<string, string[]>()
    for (const group of groups) {
        graph.set(
            group.id,
            group.choices
                .map((choice) => choice.nextGroupId)
                .filter((targetId): targetId is string => Boolean(targetId)),
        )
    }

    const visiting = new Set<string>()
    const visited = new Set<string>()

    const dfs = (groupId: string) => {
        if (visiting.has(groupId)) {
            throw new AppError("Configurator contains a cycle between option groups", 400)
        }
        if (visited.has(groupId)) return

        visiting.add(groupId)
        for (const nextGroupId of graph.get(groupId) ?? []) {
            dfs(nextGroupId)
        }
        visiting.delete(groupId)
        visited.add(groupId)
    }

    for (const group of groups) {
        dfs(group.id)
    }
}

function validateTree(optionGroups: NormalizedGroup[], steps: NormalizedStep[]) {
    const groupIds = new Set(optionGroups.map((group) => group.id))

    for (const group of optionGroups) {
        validateSelectionMode(group)
        for (const choice of group.choices) {
            if (choice.nextGroupId && !groupIds.has(choice.nextGroupId)) {
                throw new AppError(`Choice ${group.code}.${choice.code} references an unknown next group`, 400)
            }
        }
    }

    for (const step of steps) {
        if (step.minSelections > step.maxSelections) {
            throw new AppError(`Step ${step.code} has minSelections greater than maxSelections`, 400)
        }
        if (step.maxSelections > step.repeatCount) {
            throw new AppError(`Step ${step.code} cannot allow more selections than repeatCount`, 400)
        }
        if (step.entryGroupId && !groupIds.has(step.entryGroupId)) {
            throw new AppError(`Step ${step.code} references an unknown entry group`, 400)
        }
    }

    detectCycles(optionGroups)
}

async function assertUniqueConfiguratorName(shopId: string, name: string, excludeId?: string) {
    const existing = await prisma.productConfigurator.findFirst({
        where: {
            shopId,
            name,
            ...(excludeId ? { NOT: { id: excludeId } } : {}),
        },
        select: { id: true },
    })

    if (existing) {
        throw new AppError("A configurator with this name already exists in this shop", 409)
    }
}

async function fetchConfiguratorOrThrow(shopId: string, id: string) {
    const configurator = await prisma.productConfigurator.findFirst({
        where: { id, shopId },
        include: CONFIGURATOR_DETAIL_INCLUDE,
    })

    if (!configurator) {
        throw new AppError("Product configurator not found", 404)
    }

    return configurator
}

async function replaceConfiguratorTree(
    tx: typeof prisma,
    configuratorId: string,
    optionGroups: NormalizedGroup[],
    steps: NormalizedStep[],
) {
    await tx.productConfiguratorStep.deleteMany({ where: { configuratorId } })
    await tx.productOptionChoice.deleteMany({ where: { group: { configuratorId } } })
    await tx.productOptionGroup.deleteMany({ where: { configuratorId } })

    if (optionGroups.length > 0) {
        await tx.productOptionGroup.createMany({
            data: optionGroups.map((group) => ({
                id: group.id,
                configuratorId,
                code: group.code,
                label: group.label,
                selectionMode: group.selectionMode,
                minChoices: group.minChoices,
                maxChoices: group.maxChoices,
                displayOrder: group.displayOrder,
                active: group.active,
            })),
        })
    }

    const allChoices = optionGroups.flatMap((group) =>
        group.choices.map((choice) => ({
            id: choice.id,
            groupId: group.id,
            code: choice.code,
            label: choice.label,
            description: choice.description,
            priceDelta: choice.priceDelta,
            displayOrder: choice.displayOrder,
            isDefault: choice.isDefault,
            active: choice.active,
            nextGroupId: choice.nextGroupId,
        })),
    )

    if (allChoices.length > 0) {
        await tx.productOptionChoice.createMany({ data: allChoices })
    }

    if (steps.length > 0) {
        await tx.productConfiguratorStep.createMany({
            data: steps.map((step) => ({
                id: step.id,
                configuratorId,
                code: step.code,
                label: step.label,
                displayOrder: step.displayOrder,
                minSelections: step.minSelections,
                maxSelections: step.maxSelections,
                repeatCount: step.repeatCount,
                entryGroupId: step.entryGroupId,
            })),
        })
    }
}

export const productConfiguratorService = {
    async findAll(shopId: string) {
        const configurators = await prisma.productConfigurator.findMany({
            where: { shopId },
            include: CONFIGURATOR_DETAIL_INCLUDE,
            orderBy: [{ active: "desc" }, { name: "asc" }],
        })

        return configurators.map(serializeConfigurator)
    },

    async findById(shopId: string, id: string) {
        const configurator = await fetchConfiguratorOrThrow(shopId, id)
        return serializeConfigurator(configurator)
    },

    async create(shopId: string, data: CreateProductConfiguratorInput) {
        await assertUniqueConfiguratorName(shopId, data.name.trim())

        const optionGroups = normalizeGroups(data.optionGroups ?? [])
        const steps = normalizeSteps(data.steps ?? [])
        validateTree(optionGroups, steps)

        const createdId = await prisma.$transaction(async (tx) => {
            const configurator = await tx.productConfigurator.create({
                data: {
                    shopId,
                    name: data.name.trim(),
                    description: normalizeNullableText(data.description),
                    active: data.active,
                },
                select: { id: true },
            })

            await replaceConfiguratorTree(tx as typeof prisma, configurator.id, optionGroups, steps)
            return configurator.id
        })

        return this.findById(shopId, createdId)
    },

    async update(shopId: string, id: string, data: UpdateProductConfiguratorInput) {
        const existing = await fetchConfiguratorOrThrow(shopId, id)

        if (data.name !== undefined && data.name.trim() !== existing.name) {
            await assertUniqueConfiguratorName(shopId, data.name.trim(), id)
        }

        const nextOptionGroups = data.optionGroups !== undefined
            ? normalizeGroups(data.optionGroups)
            : normalizeGroups((existing.optionGroups ?? []).map((group: any) => ({
                id: group.id,
                code: group.code,
                label: group.label,
                selectionMode: group.selectionMode as ProductOptionSelectionMode,
                minChoices: group.minChoices,
                maxChoices: group.maxChoices,
                displayOrder: group.displayOrder,
                active: group.active,
                choices: (group.choices ?? []).map((choice: any) => ({
                    id: choice.id,
                    code: choice.code,
                    label: choice.label,
                    description: choice.description,
                    priceDelta: choice.priceDelta,
                    displayOrder: choice.displayOrder,
                    isDefault: choice.isDefault,
                    nextGroupId: choice.nextGroupId,
                    active: choice.active,
                })),
            })))

        const nextSteps = data.steps !== undefined
            ? normalizeSteps(data.steps)
            : normalizeSteps((existing.steps ?? []).map((step: any) => ({
                id: step.id,
                code: step.code,
                label: step.label,
                displayOrder: step.displayOrder,
                minSelections: step.minSelections,
                maxSelections: step.maxSelections,
                repeatCount: step.repeatCount,
                entryGroupId: step.entryGroupId,
            })))

        validateTree(nextOptionGroups, nextSteps)

        await prisma.$transaction(async (tx) => {
            await tx.productConfigurator.update({
                where: { id },
                data: {
                    ...(data.name !== undefined ? { name: data.name.trim() } : {}),
                    ...(data.description !== undefined ? { description: normalizeNullableText(data.description) } : {}),
                    ...(data.active !== undefined ? { active: data.active } : {}),
                },
            })

            if (data.optionGroups !== undefined || data.steps !== undefined) {
                await replaceConfiguratorTree(tx as typeof prisma, id, nextOptionGroups, nextSteps)
            }
        })

        return this.findById(shopId, id)
    },

    async remove(shopId: string, id: string) {
        const configurator = await fetchConfiguratorOrThrow(shopId, id)

        if ((configurator.products?.length ?? 0) > 0) {
            throw new AppError("Detach this configurator from linked products before deleting it", 409)
        }

        await prisma.productConfigurator.delete({ where: { id } })
    },
}

