import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'nestjs-prisma';
import { validateDslGraph } from './dsl-validate.util';
import { TemporalService } from './temporal.service';
import { verifyWebhookSignature } from './webhook-signature.util';

@Injectable()
export class WorkflowService {
  private readonly logger = new Logger(WorkflowService.name);

  constructor(
    private prisma: PrismaService,
    private temporalService: TemporalService,
  ) {}

  /**
   * Find the cron_trigger node config in a DSL, if any.
   * Returns null when no cron_trigger node or when its `cron` field is missing.
   */
  private extractCronConfig(
    dsl: any,
  ): { cron: string; timezone?: string } | null {
    const node = dsl?.nodes?.find((n: any) => n?.type === 'cron_trigger');
    if (!node?.config?.cron) return null;
    return { cron: node.config.cron, timezone: node.config.timezone };
  }

  /**
   * Find all webhook_trigger configs in a DSL.
   * Each entry includes path + method; used for collision detection on publish.
   */
  private extractWebhookConfigs(
    dsl: any,
  ): Array<{ path: string; method: string; secret?: string }> {
    if (!Array.isArray(dsl?.nodes)) return [];
    return dsl.nodes
      .filter(
        (n: any) =>
          n?.type === 'webhook_trigger' && n?.config?.path && n?.config?.method,
      )
      .map((n: any) => ({
        path: n.config.path,
        method: n.config.method,
        secret: n.config.secret,
      }));
  }

  async findMany(params: {
    status?: string;
    instanceStatus?: string;
    name?: string;
    skip?: number;
    limit?: number;
  }) {
    const where: Prisma.WorkflowWhereInput = {};
    if (params.status) {
      where.status = params.status as any;
    }
    if (params.instanceStatus) {
      where.instances = { some: { status: params.instanceStatus as any } };
    }
    if (params.name) {
      where.name = { contains: params.name, mode: 'insensitive' };
    }

    const [items, total] = await Promise.all([
      this.prisma.workflow.findMany({
        where,
        orderBy: { updatedAt: 'desc' },
        skip: params.skip || 0,
        take: params.limit || 20,
        include: {
          versions: {
            orderBy: { version: 'desc' },
            take: 1,
            select: { version: true, changeSummary: true },
          },
          _count: { select: { instances: true } },
        },
      }),
      this.prisma.workflow.count({ where }),
    ]);

    // Add per-status instance counts
    const workflowIds = items.map((w) => w.id);
    const instanceCounts = workflowIds.length
      ? await this.prisma.workflowInstance.groupBy({
          by: ['workflowId', 'status'],
          where: { workflowId: { in: workflowIds } },
          _count: true,
        })
      : [];

    const runsMap = new Map<
      bigint,
      { pending: number; running: number; completed: number; failed: number }
    >();
    for (const row of instanceCounts) {
      if (!runsMap.has(row.workflowId)) {
        runsMap.set(row.workflowId, {
          pending: 0,
          running: 0,
          completed: 0,
          failed: 0,
        });
      }
      const entry = runsMap.get(row.workflowId)!;
      const key = row.status.toLowerCase() as keyof typeof entry;
      if (key in entry) entry[key] = row._count;
    }

    // Get last run time for each workflow
    const lastRuns = workflowIds.length
      ? await this.prisma.workflowInstance.findMany({
          where: { workflowId: { in: workflowIds } },
          orderBy: { createdAt: 'desc' },
          distinct: ['workflowId'],
          select: { workflowId: true, createdAt: true, status: true },
        })
      : [];
    const lastRunMap = new Map(lastRuns.map((r) => [r.workflowId, r]));

    const enrichedItems = items.map((item) => ({
      ...item,
      runs: runsMap.get(item.id) || {
        pending: 0,
        running: 0,
        completed: 0,
        failed: 0,
      },
      lastRun: lastRunMap.get(item.id)?.createdAt || null,
    }));

    return { items: enrichedItems, total };
  }

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

  async create(
    data: { name: string; description?: string; dsl?: any },
    creator: string,
  ) {
    const workflow = await this.prisma.workflow.create({
      data: {
        name: data.name,
        description: data.description,
        dsl: data.dsl || null,
        creator,
      },
    });

    return workflow;
  }

  async update(id: bigint, data: { name?: string; description?: string }) {
    const workflow = await this.prisma.workflow.findUnique({ where: { id } });
    if (!workflow) throw new NotFoundException('Workflow not found');
    return this.prisma.workflow.update({ where: { id }, data });
  }

  async saveDraft(id: bigint, dsl: any) {
    const workflow = await this.prisma.workflow.findUnique({ where: { id } });
    if (!workflow) throw new NotFoundException('Workflow not found');
    return this.prisma.workflow.update({
      where: { id },
      data: { dsl, status: 'DRAFT' },
    });
  }

  async toggleEnabled(id: bigint, enabled: boolean) {
    const workflow = await this.prisma.workflow.findUnique({ where: { id } });
    if (!workflow) throw new NotFoundException('Workflow not found');
    const updated = await this.prisma.workflow.update({
      where: { id },
      data: { isEnabled: enabled },
    });

    // Cron schedule lifecycle: pause when disabled, resume when re-enabled
    try {
      if (enabled) {
        await this.temporalService.unpauseCronSchedule(Number(id));
      } else {
        await this.temporalService.pauseCronSchedule(Number(id));
      }
    } catch (err: any) {
      this.logger.warn(
        `Failed to ${
          enabled ? 'unpause' : 'pause'
        } cron schedule for workflow ${id}: ${err?.message ?? err}`,
      );
    }

    return updated;
  }

  async remove(id: bigint) {
    const workflow = await this.prisma.workflow.findUnique({ where: { id } });
    if (!workflow) throw new NotFoundException('Workflow not found');

    // Best-effort: delete cron schedule before removing the workflow row
    try {
      await this.temporalService.deleteCronSchedule(Number(id));
    } catch (err: any) {
      this.logger.warn(
        `Failed to delete cron schedule for workflow ${id}: ${
          err?.message ?? err
        }`,
      );
    }

    return this.prisma.workflow.delete({ where: { id } });
  }

  async getVersions(workflowId: bigint) {
    return this.prisma.workflowVersion.findMany({
      where: { workflowId },
      orderBy: { version: 'desc' },
      select: {
        id: true,
        version: true,
        prompt: true,
        changeSummary: true,
        creator: true,
        createdAt: true,
      },
    });
  }

  async getVersionDsl(workflowId: bigint, version: number) {
    const v = await this.prisma.workflowVersion.findUnique({
      where: { workflowId_version: { workflowId, version } },
    });
    if (!v) throw new NotFoundException('Version not found');
    return v;
  }

  async publish(
    workflowId: bigint,
    data: { changeSummary: string },
    creator: string,
  ) {
    const workflow = await this.prisma.workflow.findUnique({
      where: { id: workflowId },
    });
    if (!workflow) throw new NotFoundException('Workflow not found');

    // Use workflow.dsl if available; otherwise fall back to latest version's DSL
    let dslToPublish = workflow.dsl;
    if (!dslToPublish) {
      const latestVersion = await this.prisma.workflowVersion.findFirst({
        where: { workflowId },
        orderBy: { version: 'desc' },
        select: { dsl: true },
      });
      dslToPublish = latestVersion?.dsl ?? null;
    }
    if (!dslToPublish) {
      throw new NotFoundException('No draft DSL to publish');
    }

    // Structural + cycle validation. Reject malformed/cyclic DSL up-front so the
    // worker's executeGraph never has to deal with them at runtime.
    try {
      validateDslGraph(dslToPublish);
    } catch (err: any) {
      throw new BadRequestException(`Invalid DSL: ${err?.message ?? err}`);
    }

    // Validate webhook path collisions across other published workflows
    const webhookConfigs = this.extractWebhookConfigs(dslToPublish);
    if (webhookConfigs.length > 0) {
      const otherPublished = await this.prisma.workflow.findMany({
        where: { status: 'PUBLISHED', id: { not: workflowId } },
        select: { id: true, name: true, dsl: true },
      });
      for (const cfg of webhookConfigs) {
        if (!cfg.secret) {
          throw new Error(
            `webhook_trigger path="${cfg.method} ${cfg.path}" requires a secret (HMAC strict mode)`,
          );
        }
        const conflict = otherPublished.find((w) =>
          this.extractWebhookConfigs(w.dsl).some(
            (other) => other.path === cfg.path && other.method === cfg.method,
          ),
        );
        if (conflict) {
          throw new Error(
            `webhook path "${cfg.method} ${cfg.path}" already used by workflow #${conflict.id} (${conflict.name})`,
          );
        }
      }
    }

    const result = await this.prisma.$transaction(async (tx) => {
      const lastVersion = await tx.workflowVersion.findFirst({
        where: { workflowId },
        orderBy: { version: 'desc' },
        select: { version: true },
      });
      const nextVersion = (lastVersion?.version ?? 0) + 1;

      const version = await tx.workflowVersion.create({
        data: {
          workflowId,
          version: nextVersion,
          dsl: dslToPublish,
          prompt: data.changeSummary,
          changeSummary: data.changeSummary,
          creator,
        },
      });

      await tx.workflow.update({
        where: { id: workflowId },
        data: { status: 'PUBLISHED' },
      });

      return version;
    });

    // Cron schedule lifecycle: register/update if cron_trigger present, delete otherwise
    const cronConfig = this.extractCronConfig(dslToPublish);
    try {
      if (cronConfig) {
        await this.temporalService.upsertCronSchedule({
          workflowId: Number(workflowId),
          versionId: Number(result.id),
          dsl: dslToPublish,
          cron: cronConfig.cron,
          timezone: cronConfig.timezone,
        });
      } else {
        // No cron_trigger in this version → clean up any leftover schedule
        await this.temporalService.deleteCronSchedule(Number(workflowId));
      }
    } catch (err: any) {
      this.logger.warn(
        `Cron schedule sync failed for workflow ${workflowId}: ${
          err?.message ?? err
        }`,
      );
    }

    return result;
  }

  async getStats(workflowId: bigint) {
    const [running, failed, completed, total] = await Promise.all([
      this.prisma.workflowInstance.count({
        where: { workflowId, status: 'RUNNING' },
      }),
      this.prisma.workflowInstance.count({
        where: { workflowId, status: 'FAILED' },
      }),
      this.prisma.workflowInstance.count({
        where: { workflowId, status: 'COMPLETED' },
      }),
      this.prisma.workflowInstance.count({ where: { workflowId } }),
    ]);
    return { running, failed, completed, total };
  }

  async getOverview() {
    const now = new Date();
    const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1000);

    const [
      totalAll,
      total24h,
      completedAll,
      completed24h,
      failedAll,
      failed24h,
      running,
      pending,
    ] = await Promise.all([
      this.prisma.workflowInstance.count(),
      this.prisma.workflowInstance.count({
        where: { createdAt: { gte: last24h } },
      }),
      this.prisma.workflowInstance.count({ where: { status: 'COMPLETED' } }),
      this.prisma.workflowInstance.count({
        where: { status: 'COMPLETED', createdAt: { gte: last24h } },
      }),
      this.prisma.workflowInstance.count({ where: { status: 'FAILED' } }),
      this.prisma.workflowInstance.count({
        where: { status: 'FAILED', createdAt: { gte: last24h } },
      }),
      this.prisma.workflowInstance.count({ where: { status: 'RUNNING' } }),
      this.prisma.workflowInstance.count({ where: { status: 'PENDING' } }),
    ]);

    // Success rate calculation
    const denomAll = completedAll + failedAll;
    const denom24h = completed24h + failed24h;
    const successRateAll = denomAll > 0 ? (completedAll / denomAll) * 100 : 0;
    const successRate24h = denom24h > 0 ? (completed24h / denom24h) * 100 : 0;

    // AVG/P95 duration via raw SQL
    const durationAll = await this.prisma.$queryRaw<
      { avg: number | null; p95: number | null }[]
    >`
      SELECT
        AVG(EXTRACT(EPOCH FROM finished_at - started_at)) as avg,
        PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM finished_at - started_at)) as p95
      FROM t_workflow_instance
      WHERE status = 'COMPLETED' AND finished_at IS NOT NULL AND started_at IS NOT NULL
    `;

    const duration24h = await this.prisma.$queryRaw<
      { avg: number | null; p95: number | null }[]
    >`
      SELECT
        AVG(EXTRACT(EPOCH FROM finished_at - started_at)) as avg,
        PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM finished_at - started_at)) as p95
      FROM t_workflow_instance
      WHERE status = 'COMPLETED' AND finished_at IS NOT NULL AND started_at IS NOT NULL
        AND created_at >= ${last24h}
    `;

    return {
      totalRuns: { last24h: total24h, total: totalAll },
      successRate: {
        last24h: Math.round(successRate24h * 10) / 10,
        total: Math.round(successRateAll * 10) / 10,
      },
      failed: { last24h: failed24h, total: failedAll },
      duration: {
        last24h: {
          avg: Number(duration24h[0]?.avg) || 0,
          p95: Number(duration24h[0]?.p95) || 0,
        },
        total: {
          avg: Number(durationAll[0]?.avg) || 0,
          p95: Number(durationAll[0]?.p95) || 0,
        },
      },
      running,
      pending,
    };
  }

  async getInstances(
    workflowId: bigint,
    params: { status?: string; skip?: number; limit?: number },
  ) {
    const where: any = { workflowId };
    if (params.status) where.status = params.status;

    const [items, total] = await Promise.all([
      this.prisma.workflowInstance.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip: params.skip || 0,
        take: params.limit || 20,
      }),
      this.prisma.workflowInstance.count({ where }),
    ]);

    return { items, total };
  }

  // ─── Execution Methods ──────────────────────────────────────────────────────

  async executeWorkflow(
    workflowId: bigint,
    dto: { context?: Record<string, any>; versionId?: number },
    creator: string,
    temporalService: any,
  ) {
    // Get the workflow and its latest published version
    const workflow = await this.prisma.workflow.findUnique({
      where: { id: workflowId },
      include: {
        versions: {
          orderBy: { version: 'desc' },
          take: 1,
        },
      },
    });

    if (!workflow) throw new NotFoundException('Workflow not found');
    if (workflow.status !== 'PUBLISHED') {
      throw new Error('Workflow must be published before execution');
    }
    if (!workflow.isEnabled) {
      throw new Error('Workflow is disabled');
    }

    // Determine which version to use
    let version;
    if (dto.versionId) {
      version = await this.prisma.workflowVersion.findFirst({
        where: { workflowId, version: dto.versionId },
      });
    } else {
      version = workflow.versions[0];
    }

    if (!version) throw new NotFoundException('No published version found');

    // Create a WorkflowInstance record
    const instance = await this.prisma.workflowInstance.create({
      data: {
        workflowId,
        versionId: version.id,
        status: 'PENDING',
        context: dto.context || {},
        creator,
      },
    });

    // Start the Temporal workflow
    const { temporalRunId } = await temporalService.startWorkflow({
      instanceId: Number(instance.id),
      workflowId: Number(workflowId),
      versionId: Number(version.id),
      dsl: version.dsl,
      context: dto.context || {},
    });

    // Update instance with Temporal run ID
    await this.prisma.workflowInstance.update({
      where: { id: instance.id },
      data: { temporalRunId },
    });

    return {
      instanceId: instance.id.toString(),
      temporalRunId,
      status: 'PENDING',
    };
  }

  /**
   * Webhook trigger entry point. Looks up published workflows whose DSL contains a
   * webhook_trigger node matching (path, method), verifies HMAC signature against
   * each match's secret, and starts a workflow instance per verified match.
   *
   * Returns the list of created instance IDs. Throws:
   * - NotFoundException when no published workflow listens on this path/method
   * - UnauthorizedException on bad signature or stale timestamp
   */
  async receiveWebhook(input: {
    path: string;
    method: string;
    signatureHeader: string | undefined;
    timestampHeader: string | undefined;
    rawBody: Buffer;
    body: any;
    headers: Record<string, any>;
    query: Record<string, any>;
  }): Promise<{ instanceIds: string[] }> {
    // 1) Timestamp window — reject stale requests (±5 min)
    const tsMs = Number(input.timestampHeader) * 1000;
    if (
      !input.timestampHeader ||
      !Number.isFinite(tsMs) ||
      Math.abs(Date.now() - tsMs) > 5 * 60 * 1000
    ) {
      throw new UnauthorizedException(
        'Invalid or expired X-Workflow-Timestamp',
      );
    }

    // 2) Lookup matching published + enabled workflows
    const published = await this.prisma.workflow.findMany({
      where: { status: 'PUBLISHED', isEnabled: true },
      include: {
        versions: { orderBy: { version: 'desc' }, take: 1 },
      },
    });

    type Match = {
      workflowId: bigint;
      versionId: bigint;
      dsl: any;
      secret: string;
    };
    const matches: Match[] = [];
    for (const wf of published) {
      const version = wf.versions[0];
      const dsl = version?.dsl ?? wf.dsl;
      if (!dsl) continue;
      const cfgs = this.extractWebhookConfigs(dsl);
      const cfg = cfgs.find(
        (c) => c.path === input.path && c.method === input.method,
      );
      if (!cfg || !cfg.secret) continue;
      matches.push({
        workflowId: wf.id,
        versionId: version?.id ?? wf.id,
        dsl,
        secret: cfg.secret,
      });
    }

    if (matches.length === 0) {
      throw new NotFoundException(
        `No published workflow listens on ${input.method} /${input.path}`,
      );
    }

    // 3) Verify HMAC against each match's secret. Strict mode: every match must verify.
    for (const m of matches) {
      const ok = verifyWebhookSignature(
        m.secret,
        input.timestampHeader!,
        input.rawBody,
        input.signatureHeader,
      );
      if (!ok) {
        throw new UnauthorizedException('Invalid X-Workflow-Signature');
      }
    }

    // 4) Trigger executions in parallel
    const context = {
      headers: input.headers,
      query: input.query,
      body: input.body,
      receivedAt: new Date().toISOString(),
    };
    const results = await Promise.all(
      matches.map((m) =>
        this.executeWorkflow(
          m.workflowId,
          { context },
          'webhook',
          this.temporalService,
        ),
      ),
    );

    return { instanceIds: results.map((r) => r.instanceId) };
  }

  async getInstanceDetail(instanceId: bigint) {
    const instance = await this.prisma.workflowInstance.findUnique({
      where: { id: instanceId },
      include: {
        workflow: { select: { name: true, dsl: true } },
        nodeExecutions: {
          orderBy: { createdAt: 'asc' },
        },
      },
    });

    if (!instance) throw new NotFoundException('Instance not found');

    // Fetch the workflow DSL from the version
    let dsl: any = null;
    if (instance.versionId) {
      const version = await this.prisma.workflowVersion.findUnique({
        where: { id: instance.versionId },
        select: { dsl: true },
      });
      dsl = version?.dsl || null;
    }

    // Fallback: use DSL from the workflow itself
    if (!dsl && instance.workflow.dsl) {
      dsl = instance.workflow.dsl;
    }

    return {
      ...instance,
      workflow: { name: instance.workflow.name },
      dsl,
    };
  }

  async cancelInstance(instanceId: bigint, temporalService: any) {
    const instance = await this.prisma.workflowInstance.findUnique({
      where: { id: instanceId },
    });

    if (!instance) throw new NotFoundException('Instance not found');
    if (instance.status !== 'RUNNING' && instance.status !== 'PENDING') {
      throw new Error('Can only cancel RUNNING or PENDING instances');
    }

    if (instance.temporalRunId) {
      await temporalService.cancelWorkflow(instance.temporalRunId);
    }

    await this.prisma.workflowInstance.update({
      where: { id: instanceId },
      data: { status: 'CANCELLED', finishedAt: new Date() },
    });

    return { success: true };
  }

  async retryInstance(
    instanceId: bigint,
    creator: string,
    temporalService: any,
  ) {
    const instance = await this.prisma.workflowInstance.findUnique({
      where: { id: instanceId },
    });

    if (!instance) throw new NotFoundException('Instance not found');
    if (instance.status !== 'FAILED') {
      throw new Error('Can only retry FAILED instances');
    }

    // Create a new instance with the same context
    return this.executeWorkflow(
      instance.workflowId,
      {
        context: instance.context as Record<string, any>,
        versionId: undefined,
      },
      creator,
      temporalService,
    );
  }

  async signalInstance(
    instanceId: bigint,
    dto: { signalName: string; payload: Record<string, any> },
    temporalService: any,
  ) {
    const instance = await this.prisma.workflowInstance.findUnique({
      where: { id: instanceId },
    });

    if (!instance) throw new NotFoundException('Instance not found');
    if (!instance.temporalRunId) {
      throw new Error('Instance has no Temporal run ID');
    }

    await temporalService.signalWorkflow(
      instance.temporalRunId,
      dto.signalName,
      dto.payload,
    );
    return { success: true };
  }
}
