import { createHmac, timingSafeEqual } from 'node:crypto';
import { NOTIFICATION_KINDS, SECURITY_TIERS } from '@pig/core';
import type { Database } from '@pig/db';
import { accounts, channelLinks, notificationOutbox } from '@pig/db';
import { and, desc, eq, inArray } from 'drizzle-orm';
import { Hono, type Context } from 'hono';
import { z } from 'zod';
import type { Config } from '../lib/config';
import { requireCapability } from '../lib/auth';
import {
  bodylessMutation,
  MutationError,
  mutation,
  type ApiEnv,
  type MutationDefinition,
} from '../lib/mutation';
import type { CapacityService } from '../services/capacity';

export const SLACK_CAPACITY_COMMAND_PATH = '/api/integrations/slack/commands/capacity-match';
const SLACK_REPLAY_WINDOW_SECONDS = 5 * 60;

const slackLinkSchema = z
  .object({
    workspaceId: z.string().trim().min(1).max(80),
    channelId: z.string().trim().min(1).max(80),
    channelName: z.string().trim().min(1).max(120).nullable().optional(),
    accountId: z.string().uuid(),
    notifyOn: z.array(z.enum(NOTIFICATION_KINDS)).min(1).default([...NOTIFICATION_KINDS]),
  })
  .strict();

const slashFormSchema = z.object({
  team_id: z.string().min(1),
  channel_id: z.string().min(1),
  user_id: z.string().min(1),
  text: z.string().default(''),
});

const slashRequirementSchema = z.object({
  gpuCount: z.number().int().positive(),
  gpuType: z.string().min(1),
  totalGpuHours: z.number().positive().optional(),
  requiresHighSpeedInterconnect: z.boolean().optional(),
  minSecurityTier: z.enum(SECURITY_TIERS).optional(),
  maxPricePerGpuHourCents: z.number().int().positive().optional(),
});
const emptyMutationSchema = z.object({}).strict();

export function createSlackLinkMutationDefinition(): MutationDefinition<
  typeof slackLinkSchema,
  typeof channelLinks.$inferSelect
> {
  return {
    schema: slackLinkSchema,
    permission: { capability: 'settings:admin' },
    invalidMessage: 'Invalid Slack channel link.',
    async mutate({ input, principal, tx, now }) {
      const [account] = await tx
        .select({ id: accounts.id, name: accounts.name })
        .from(accounts)
        .where(eq(accounts.id, input.accountId))
        .limit(1);
      if (!account) throw MutationError.notFound('Account');

      const [link] = await tx
        .insert(channelLinks)
        .values({
          ...input,
          platform: 'slack',
          linkedByUserId: principal.userId,
          updatedAt: now,
        })
        .onConflictDoUpdate({
          target: [channelLinks.platform, channelLinks.workspaceId, channelLinks.channelId],
          set: {
            accountId: input.accountId,
            channelName: input.channelName,
            notifyOn: input.notifyOn,
            linkedByUserId: principal.userId,
            updatedAt: now,
          },
        })
        .returning();
      if (!link) throw new MutationError('write_failed', 'Slack channel was not linked.', 409);
      return {
        data: link,
        activity: {
          type: 'slack',
          subject: `Linked Slack channel to ${account.name}`,
          accountId: account.id,
          meta: { action: 'linked', integration: 'slack', channelId: link.channelId },
        },
      };
    },
  };
}

function createSlackUnlinkMutationDefinition(): MutationDefinition<
  typeof emptyMutationSchema,
  { deleted: true }
> {
  return {
    schema: emptyMutationSchema,
    permission: { capability: 'settings:admin' },
    invalidMessage: 'Invalid Slack unlink request.',
    async mutate({ params, tx }) {
      if (!params.id) throw MutationError.notFound('Slack channel link');
      const [link] = await tx
        .select()
        .from(channelLinks)
        .where(and(eq(channelLinks.id, params.id), eq(channelLinks.platform, 'slack')))
        .limit(1);
      if (!link) throw MutationError.notFound('Slack channel link');

      await tx
        .update(notificationOutbox)
        .set({ status: 'cancelled', leasedBy: null, leasedUntil: null })
        .where(
          and(
            eq(notificationOutbox.linkId, link.id),
            inArray(notificationOutbox.status, ['pending', 'leased']),
          ),
        );
      await tx.delete(channelLinks).where(eq(channelLinks.id, link.id));
      return {
        data: { deleted: true as const },
        activity: {
          type: 'slack',
          subject: 'Unlinked Slack channel',
          accountId: link.accountId ?? undefined,
          meta: { action: 'unlinked', integration: 'slack', channelId: link.channelId },
        },
      };
    },
  };
}

export function createSlackRoutes(
  config: Config,
  db: Database,
  capacity: CapacityService,
): Hono<ApiEnv> {
  const app = new Hono<ApiEnv>();

  app.post(SLACK_CAPACITY_COMMAND_PATH, async (c) => {
    const rawBody = await c.req.text();
    if (
      !config.SLACK_SIGNING_SECRET ||
      !verifySlackRequest(
        config.SLACK_SIGNING_SECRET,
        c.req.header('x-slack-request-timestamp'),
        c.req.header('x-slack-signature'),
        rawBody,
      )
    ) {
      return c.json({ error: 'Invalid Slack signature.' }, 401);
    }

    const form = slashFormSchema.safeParse(Object.fromEntries(new URLSearchParams(rawBody)));
    if (!form.success) return slackEphemeral(c, 'Slack sent an incomplete command request.');

    const [link] = await db
      .select({ id: channelLinks.id })
      .from(channelLinks)
      .where(
        and(
          eq(channelLinks.platform, 'slack'),
          eq(channelLinks.workspaceId, form.data.team_id),
          eq(channelLinks.channelId, form.data.channel_id),
        ),
      )
      .limit(1);
    if (!link) {
      return slackEphemeral(c, 'Link this channel to a PIG account before matching capacity.');
    }

    const requirement = parseSlashRequirement(form.data.text);
    if (!requirement) {
      return slackEphemeral(
        c,
        'Usage: `/pig-capacity 8 H100_80GB hours=640 max=2.50 fabric tier=secure_cloud`',
      );
    }
    const matches = await capacity.match(requirement);
    return slackEphemeral(c, formatCapacityMatches(requirement, matches));
  });

  app.get('/api/integrations/slack/channel-links', async (c) => {
    requireCapability(c.get('principal'), 'settings:admin');
    const links = await db
      .select({ link: channelLinks, accountName: accounts.name })
      .from(channelLinks)
      .innerJoin(accounts, eq(accounts.id, channelLinks.accountId))
      .where(eq(channelLinks.platform, 'slack'))
      .orderBy(desc(channelLinks.updatedAt));
    return c.json(links);
  });
  app.post(
    '/api/integrations/slack/channel-links',
    mutation(db, createSlackLinkMutationDefinition()),
  );
  app.delete(
    '/api/integrations/slack/channel-links/:id',
    bodylessMutation(db, createSlackUnlinkMutationDefinition()),
  );

  return app;
}

export function verifySlackRequest(
  signingSecret: string,
  timestampHeader: string | undefined,
  signatureHeader: string | undefined,
  rawBody: string,
  nowSeconds = Math.floor(Date.now() / 1_000),
): boolean {
  if (!timestampHeader || !signatureHeader || !/^v0=[a-f0-9]{64}$/i.test(signatureHeader)) {
    return false;
  }
  const timestamp = Number(timestampHeader);
  if (!Number.isSafeInteger(timestamp)) return false;
  if (Math.abs(nowSeconds - timestamp) > SLACK_REPLAY_WINDOW_SECONDS) return false;

  const expected = `v0=${createHmac('sha256', signingSecret)
    .update(`v0:${timestampHeader}:${rawBody}`)
    .digest('hex')}`;
  const actualBuffer = Buffer.from(signatureHeader, 'utf8');
  const expectedBuffer = Buffer.from(expected, 'utf8');
  return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
}

export function parseSlashRequirement(text: string): z.infer<typeof slashRequirementSchema> | null {
  const tokens = text.trim().split(/\s+/).filter(Boolean);
  const countToken = tokens.shift();
  const gpuType = tokens.shift();
  if (!countToken || !gpuType) return null;
  const gpuCount = Number(countToken.replace(/x$/i, ''));

  const candidate: Record<string, unknown> = { gpuCount, gpuType };
  for (const token of tokens) {
    if (token === 'fabric') candidate.requiresHighSpeedInterconnect = true;
    else if (token.startsWith('hours=')) candidate.totalGpuHours = Number(token.slice(6));
    else if (token.startsWith('max=')) {
      candidate.maxPricePerGpuHourCents = Math.round(Number(token.slice(4)) * 100);
    } else if (token.startsWith('tier=')) candidate.minSecurityTier = token.slice(5);
    else return null;
  }
  const parsed = slashRequirementSchema.safeParse(candidate);
  return parsed.success ? parsed.data : null;
}

function formatCapacityMatches(
  requirement: z.infer<typeof slashRequirementSchema>,
  matches: Awaited<ReturnType<CapacityService['match']>>,
): string {
  if (matches.length === 0) {
    return `No sellable capacity matches ${requirement.gpuCount}× ${requirement.gpuType}.`;
  }
  const lines = matches.slice(0, 5).map((match, index) => {
    const breakEven =
      match.breakEvenPriceCents == null
        ? 'break-even unavailable'
        : `$${(match.breakEvenPriceCents / 100).toFixed(2)}/GPU-hour break-even`;
    return `${index + 1}. *${match.name}* — ${match.gpuCount}× ${match.gpuType}, ${Math.round(match.availableGpuHours)} hours available, ${breakEven}, ${Math.round(match.score * 100)}% fit`;
  });
  return [`*Best capacity matches for ${requirement.gpuCount}× ${requirement.gpuType}*`, ...lines].join('\n');
}

function slackEphemeral(c: Context<ApiEnv>, text: string) {
  return c.json({ response_type: 'ephemeral', text });
}
