import {
  BadRequestException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
import { PrismaService } from 'nestjs-prisma';
import { ExportPayload, ImportPreviewResponse } from './workflow-export.dto';

const EXPORT_TOKEN_SECRET =
  process.env.TAIHU_ODC_APP_TOKEN || 'gadmin-default-secret';
const EXPORT_TOKEN_EXPIRY = 30 * 60; // 30 minutes in seconds

interface ExportTokenPayload {
  workflowCode: string;
}

@Injectable()
export class WorkflowExportService {
  constructor(private prisma: PrismaService) {}

  /** Generate a JWT export token for a workflow */
  async generateExportToken(
    workflowId: bigint,
    baseUrl: string,
  ): Promise<{ url: string }> {
    const workflow = await this.prisma.workflow.findUnique({
      where: { id: workflowId },
      select: { code: true },
    });
    if (!workflow) throw new NotFoundException('Workflow not found');

    const token = jwt.sign(
      { workflowCode: workflow.code } as ExportTokenPayload,
      EXPORT_TOKEN_SECRET,
      { expiresIn: EXPORT_TOKEN_EXPIRY },
    );

    const url = `${baseUrl}/workflow/export/${workflow.code}?token=${token}`;
    return { url };
  }

  /** Verify export token and return the full export payload */
  async getExportPayload(code: string, token: string): Promise<ExportPayload> {
    let decoded: ExportTokenPayload;
    try {
      decoded = jwt.verify(token, EXPORT_TOKEN_SECRET) as ExportTokenPayload;
    } catch {
      throw new UnauthorizedException('Export token is invalid or expired');
    }

    if (decoded.workflowCode !== code) {
      throw new UnauthorizedException('Token does not match this workflow');
    }

    const workflow = await this.prisma.workflow.findUnique({
      where: { code },
      include: { versions: { orderBy: { version: 'desc' }, take: 1 } },
    });
    if (!workflow) throw new NotFoundException('Workflow not found');

    // Use latest published version DSL, fallback to draft DSL
    const dsl = (workflow.versions[0]?.dsl ?? workflow.dsl) as any;

    // Collect referenced NodeInstance IDs from DSL
    const instanceIds = new Set<bigint>();
    if (dsl?.nodes) {
      for (const node of dsl.nodes) {
        if (node.instanceRef?.instanceId) {
          instanceIds.add(BigInt(node.instanceRef.instanceId));
        }
      }
    }

    // Fetch referenced NodeInstances
    const nodeInstances =
      instanceIds.size > 0
        ? await this.prisma.workflowNodeInstance.findMany({
            where: { id: { in: Array.from(instanceIds) } },
            include: { nodeType: true },
          })
        : [];

    // Collect NodeType keys from instances + DSL nodes
    const nodeTypeKeys = new Set<string>();
    for (const ni of nodeInstances) {
      nodeTypeKeys.add(ni.nodeType.type);
    }
    if (dsl?.nodes) {
      for (const node of dsl.nodes) {
        if (node.type) nodeTypeKeys.add(node.type);
      }
    }

    // Fetch NodeTypes
    const nodeTypes =
      nodeTypeKeys.size > 0
        ? await this.prisma.workflowNodeType.findMany({
            where: { type: { in: Array.from(nodeTypeKeys) } },
          })
        : [];

    return {
      version: 1,
      exportedAt: new Date().toISOString(),
      workflow: {
        code: workflow.code,
        name: workflow.name,
        description: workflow.description,
        status: workflow.status,
        dsl,
      },
      nodeInstances: nodeInstances.map((ni) => ({
        sourceId: String(ni.id),
        code: ni.code,
        name: ni.name,
        description: ni.description,
        nodeTypeKey: ni.nodeType.type,
        config: ni.config,
        isEnabled: ni.isEnabled,
      })),
      nodeTypes: nodeTypes.map((nt) => ({
        type: nt.type,
        category: nt.category,
        label: nt.label,
        icon: nt.icon,
        description: nt.description,
        configSchema: nt.configSchema as any,
        inputSchema: nt.inputSchema as any,
        outputSchema: nt.outputSchema as any,
        isBuiltin: nt.isBuiltin,
      })),
    };
  }

  /** Preview import — return current state vs incoming for diff */
  async importPreview(payload: ExportPayload): Promise<ImportPreviewResponse> {
    if (!payload?.workflow?.code) {
      throw new BadRequestException('Invalid payload: missing workflow code');
    }

    // Look up current state by code
    const existingWorkflow = await this.prisma.workflow.findUnique({
      where: { code: payload.workflow.code },
      include: { versions: { orderBy: { version: 'desc' }, take: 1 } },
    });

    // Look up existing node instances by code
    const incomingCodes = payload.nodeInstances.map((ni) => ni.code);
    const existingInstances =
      incomingCodes.length > 0
        ? await this.prisma.workflowNodeInstance.findMany({
            where: { code: { in: incomingCodes } },
            include: { nodeType: true },
          })
        : [];

    // Look up existing node types by type key
    const incomingTypes = payload.nodeTypes.map((nt) => nt.type);
    const existingTypes =
      incomingTypes.length > 0
        ? await this.prisma.workflowNodeType.findMany({
            where: { type: { in: incomingTypes } },
          })
        : [];

    const current: Partial<ExportPayload> | null = existingWorkflow
      ? {
          workflow: {
            code: existingWorkflow.code,
            name: existingWorkflow.name,
            description: existingWorkflow.description,
            status: existingWorkflow.status,
            dsl: (existingWorkflow.versions[0]?.dsl ??
              existingWorkflow.dsl) as any,
          },
          nodeInstances: existingInstances.map((ni) => ({
            sourceId: String(ni.id),
            code: ni.code,
            name: ni.name,
            description: ni.description,
            nodeTypeKey: ni.nodeType.type,
            config: ni.config as any,
            isEnabled: ni.isEnabled,
          })),
          nodeTypes: existingTypes.map((nt) => ({
            type: nt.type,
            category: nt.category,
            label: nt.label,
            icon: nt.icon,
            description: nt.description,
            configSchema: nt.configSchema as any,
            inputSchema: nt.inputSchema as any,
            outputSchema: nt.outputSchema as any,
            isBuiltin: nt.isBuiltin,
          })),
        }
      : null;

    return { current, incoming: payload };
  }

  /** Confirm import — execute upsert with DSL remapping in a transaction */
  async importConfirm(
    payload: ExportPayload,
    creator: string,
  ): Promise<{ success: boolean; workflowId: string; workflowName: string }> {
    if (!payload?.workflow?.code) {
      throw new BadRequestException('Invalid payload: missing workflow code');
    }

    return this.prisma.$transaction(async (tx) => {
      // 1. Upsert NodeTypes by `type`
      for (const nt of payload.nodeTypes) {
        await tx.workflowNodeType.upsert({
          where: { type: nt.type },
          create: {
            type: nt.type,
            category: nt.category as any,
            label: nt.label,
            icon: nt.icon,
            description: nt.description,
            configSchema: nt.configSchema,
            inputSchema: nt.inputSchema,
            outputSchema: nt.outputSchema,
            isBuiltin: nt.isBuiltin,
            creator,
          },
          update: {
            category: nt.category as any,
            label: nt.label,
            icon: nt.icon,
            description: nt.description,
            configSchema: nt.configSchema,
            inputSchema: nt.inputSchema,
            outputSchema: nt.outputSchema,
          },
        });
      }

      // 2. Upsert NodeInstances by `code`, build sourceId → newId map
      const idMap = new Map<string, string>();

      for (const ni of payload.nodeInstances) {
        const nodeType = await tx.workflowNodeType.findUnique({
          where: { type: ni.nodeTypeKey },
        });
        if (!nodeType) {
          throw new BadRequestException(
            `NodeType "${ni.nodeTypeKey}" not found after upsert`,
          );
        }

        const record = await tx.workflowNodeInstance.upsert({
          where: { code: ni.code },
          create: {
            code: ni.code,
            nodeTypeId: nodeType.id,
            name: ni.name,
            description: ni.description,
            config: ni.config,
            isEnabled: ni.isEnabled,
            creator,
          },
          update: {
            nodeTypeId: nodeType.id,
            name: ni.name,
            description: ni.description,
            config: ni.config,
            isEnabled: ni.isEnabled,
          },
        });

        idMap.set(ni.sourceId, String(record.id));
      }

      // 3. Remap DSL instanceRef.instanceId
      const dsl = JSON.parse(JSON.stringify(payload.workflow.dsl));
      if (dsl?.nodes) {
        for (const node of dsl.nodes) {
          if (node.instanceRef?.instanceId) {
            const newId = idMap.get(node.instanceRef.instanceId);
            if (newId) {
              node.instanceRef.instanceId = newId;
            }
          }
        }
      }

      // 4. Upsert Workflow by `code`, always set to DRAFT
      const workflow = await tx.workflow.upsert({
        where: { code: payload.workflow.code },
        create: {
          code: payload.workflow.code,
          name: payload.workflow.name,
          description: payload.workflow.description,
          status: 'DRAFT',
          dsl,
          creator,
        },
        update: {
          name: payload.workflow.name,
          description: payload.workflow.description,
          status: 'DRAFT',
          dsl,
        },
      });

      return {
        success: true,
        workflowId: String(workflow.id),
        workflowName: workflow.name,
      };
    });
  }
}
