/**
 * Staff-admin controls for terminal translation webhook deliveries.
 *
 * This surface is intentionally narrower than the worker's outbox model. It
 * exposes only due/dead terminal rows and never selects callback secrets,
 * callback URLs, or the callback body.
 */

import { NextFunction, Request, Response, Router } from 'express';
import { Prisma, PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authenticateAdmin } from '../../middleware/auth';
import { validateParams, validateQuery } from '../../middleware/validator';
import { errorResponse, successResponse } from '../../utils/errorHandler';
import { logger } from '../../utils/logger';

const router = Router();
const prisma = new PrismaClient();

const DEFAULT_PAGE_SIZE = 50;
const MAX_PAGE_SIZE = 100;
const MAX_PAGE = 10_000;

const TERMINAL_EVENT_NAMES = [
  'translation.completed',
  'translation.failed',
  'bulk_translation.completed',
  'bulk_translation.failed',
  'bulk_content_translation.completed',
  'bulk_content_translation.failed',
] as const;

const TERMINAL_JOB_STATUSES = ['completed', 'failed', 'cancelled'] as const;

type OutboxState = 'all' | 'due' | 'dead';
type OutboxStateInput = OutboxState | 'pending';

const stateFromInput = (value: OutboxStateInput | undefined): OutboxState | undefined => {
  if (value === undefined) {
    return undefined;
  }
  return value === 'pending' ? 'due' : value;
};

const rawListQuerySchema = z.object({
  page: z.coerce.number().int().min(1).max(MAX_PAGE).default(1),
  per_page: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).optional(),
  // `limit` is accepted for consistency with the older admin list endpoints.
  limit: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).optional(),
  status: z.enum(['all', 'due', 'dead', 'pending']).optional(),
  state: z.enum(['all', 'due', 'dead', 'pending']).optional(),
}).strict().superRefine((value, context) => {
  if (value.per_page !== undefined && value.limit !== undefined) {
    context.addIssue({
      code: z.ZodIssueCode.custom,
      path: ['per_page'],
      message: 'per_page and limit cannot both be provided',
    });
  }

  const status = stateFromInput(value.status);
  const state = stateFromInput(value.state);
  if (status !== undefined && state !== undefined && status !== state) {
    context.addIssue({
      code: z.ZodIssueCode.custom,
      path: ['state'],
      message: 'status and state must identify the same outbox state',
    });
  }
}).transform((value) => ({
  page: value.page,
  perPage: value.per_page ?? value.limit ?? DEFAULT_PAGE_SIZE,
  state: stateFromInput(value.state ?? value.status) ?? 'all',
}));

export const listQuerySchema = rawListQuerySchema;

const outboxParamsSchema = z.object({
  id: z.string().uuid(),
}).strict();

const emptyBodySchema = z.object({}).strict();

const SAFE_OUTBOX_SELECT = {
  id: true,
  job_id: true,
  event: true,
  delivery_sequence: true,
  is_final: true,
  status: true,
  attempts: true,
  claimed_at: true,
  next_attempt_at: true,
  first_attempt_at: true,
  last_attempt_at: true,
  dead_at: true,
  response_status: true,
  created_at: true,
  delivered_at: true,
  job: {
    select: {
      id: true,
      status: true,
    },
  },
} as const;

type SafeOutbox = Prisma.WebhookOutboxGetPayload<{
  select: typeof SAFE_OUTBOX_SELECT;
}>;

const terminalEventNames = [...TERMINAL_EVENT_NAMES];
const terminalJobStatuses = [...TERMINAL_JOB_STATUSES];

function dueWhere(now: Date): Prisma.WebhookOutboxWhereInput {
  return {
    status: 'pending',
    OR: [
      { next_attempt_at: null },
      { next_attempt_at: { lte: now } },
    ],
  };
}

export function buildEligibleWhere(state: OutboxState, now: Date): Prisma.WebhookOutboxWhereInput {
  const base: Prisma.WebhookOutboxWhereInput = {
    is_final: true,
    event: { in: terminalEventNames },
    job: {
      is: {
        status: { in: terminalJobStatuses },
      },
    },
  };

  if (state === 'due') {
    return { ...base, ...dueWhere(now) };
  }

  if (state === 'dead') {
    return { ...base, status: 'dead' };
  }

  return {
    ...base,
    OR: [
      dueWhere(now),
      { status: 'dead' },
    ],
  };
}

function isDue(row: Pick<SafeOutbox, 'next_attempt_at'>, now: Date): boolean {
  if (row.next_attempt_at === null) {
    return true;
  }

  const nextAttempt = row.next_attempt_at.getTime();
  return Number.isFinite(nextAttempt) && nextAttempt <= now.getTime();
}

function isEligibleRow(row: SafeOutbox, now: Date): boolean {
  if (!row.job || !row.is_final || !TERMINAL_EVENT_NAMES.includes(row.event as (typeof TERMINAL_EVENT_NAMES)[number])) {
    return false;
  }

  if (!TERMINAL_JOB_STATUSES.includes(row.job.status as (typeof TERMINAL_JOB_STATUSES)[number])) {
    return false;
  }

  return row.status === 'dead' || (row.status === 'pending' && isDue(row, now));
}

export function toPublicOutbox(row: SafeOutbox) {
  return {
    id: row.id,
    job_id: row.job_id,
    event: row.event,
    delivery_sequence: row.delivery_sequence,
    is_final: row.is_final,
    status: row.status,
    attempts: row.attempts,
    claimed_at: row.claimed_at?.toISOString() ?? null,
    next_attempt_at: row.next_attempt_at?.toISOString() ?? null,
    first_attempt_at: row.first_attempt_at?.toISOString() ?? null,
    last_attempt_at: row.last_attempt_at?.toISOString() ?? null,
    dead_at: row.dead_at?.toISOString() ?? null,
    response_status: row.response_status,
    created_at: row.created_at.toISOString(),
    delivered_at: row.delivered_at?.toISOString() ?? null,
    job: row.job
      ? {
          id: row.job.id,
          status: row.job.status,
        }
      : null,
  };
}

function sendValidationError(res: Response, message: string): void {
  res.status(400).json(errorResponse('VALIDATION_ERROR', message));
}

function validateEmptyRedriveBody(req: Request, res: Response, next: NextFunction): void {
  const result = emptyBodySchema.safeParse(req.body === undefined ? {} : req.body);
  if (!result.success) {
    sendValidationError(res, 'Redrive requests do not accept a request body');
    return;
  }

  next();
}

function handleOutboxError(
  error: unknown,
  req: Request,
  res: Response,
  operation: string,
  outboxId?: string,
): void {
  logger.error(operation, {
    error,
    adminId: req.admin?.id,
    outboxId,
  });
  res.status(503).json(errorResponse('OUTBOX_UNAVAILABLE', 'Webhook outbox is temporarily unavailable'));
}

/**
 * GET /v1/admin/translation-webhook-outbox
 *
 * Lists only terminal rows that are due now or dead. Pagination and state
 * filters are bounded and strict so an invalid request cannot turn into an
 * unbounded database read.
 */
router.get(
  '/',
  authenticateAdmin,
  validateQuery(listQuerySchema),
  async (req: Request, res: Response) => {
    const { page, perPage, state } = req.query as unknown as z.infer<typeof listQuerySchema>;
    const now = new Date();
    const where = buildEligibleWhere(state, now);
    const skip = (page - 1) * perPage;

    try {
      const [rows, total] = await Promise.all([
        prisma.webhookOutbox.findMany({
          where,
          select: SAFE_OUTBOX_SELECT,
          orderBy: [{ created_at: 'asc' }, { id: 'asc' }],
          skip,
          take: perPage,
        }),
        prisma.webhookOutbox.count({ where }),
      ]);

      const totalPages = total === 0 ? 0 : Math.ceil(total / perPage);
      res.status(200).json(successResponse({
        entries: rows.map(toPublicOutbox),
        pagination: {
          page,
          per_page: perPage,
          total,
          total_pages: totalPages,
          has_next: totalPages > 0 && page < totalPages,
          has_prev: page > 1 && totalPages > 0,
        },
      }));
    } catch (error) {
      handleOutboxError(error, req, res, 'Failed to list translation webhook outbox');
    }
  },
);

/**
 * GET /v1/admin/translation-webhook-outbox/:id
 *
 * Inspect one due/dead terminal row using the same safe projection as the
 * list endpoint. Non-eligible states are not exposed by this repair surface.
 */
router.get(
  '/:id',
  authenticateAdmin,
  validateParams(outboxParamsSchema),
  async (req: Request, res: Response) => {
    const { id } = req.params;
    const now = new Date();

    try {
      const row = await prisma.webhookOutbox.findUnique({
        where: { id },
        select: SAFE_OUTBOX_SELECT,
      });

      if (!row || !isEligibleRow(row, now)) {
        res.status(404).json(errorResponse('OUTBOX_NOT_FOUND', 'Eligible webhook outbox row not found'));
        return;
      }

      res.status(200).json(successResponse({ entry: toPublicOutbox(row) }));
    } catch (error) {
      handleOutboxError(error, req, res, 'Failed to inspect translation webhook outbox', id);
    }
  },
);

type RedriveTransactionResult =
  | { kind: 'missing' }
  | { kind: 'not_eligible' }
  | { kind: 'conflict' }
  | { kind: 'unavailable' }
  | { kind: 'redriven'; row: SafeOutbox };

/**
 * POST /v1/admin/translation-webhook-outbox/:id/redrive
 *
 * Redrive is an explicit state transition. It resets delivery scheduling
 * fields only; the event identity, job link, sequence, and callback body are
 * immutable. The audit row and state transition commit together.
 */
router.post(
  '/:id/redrive',
  authenticateAdmin,
  validateParams(outboxParamsSchema),
  validateEmptyRedriveBody,
  async (req: Request, res: Response) => {
    const { id } = req.params;
    const now = new Date();

    try {
      const result = await prisma.$transaction<RedriveTransactionResult>(async (tx) => {
        const current = await tx.webhookOutbox.findUnique({
          where: { id },
          select: SAFE_OUTBOX_SELECT,
        });

        if (!current) {
          return { kind: 'missing' };
        }

        if (!isEligibleRow(current, now)) {
          return { kind: 'not_eligible' };
        }

        const claimed = await tx.webhookOutbox.updateMany({
          where: {
            ...buildEligibleWhere(current.status === 'dead' ? 'dead' : 'due', now),
            id,
          },
          data: {
            status: 'pending',
            attempts: 0,
            last_error: null,
            claimed_at: null,
            next_attempt_at: now,
            first_attempt_at: null,
            last_attempt_at: null,
            dead_at: null,
            response_status: null,
            delivered_at: null,
          },
        });

        if (claimed.count !== 1) {
          return { kind: 'conflict' };
        }

        await tx.auditLog.create({
          data: {
            action: 'admin.translation_webhook_outbox.redrive',
            resource_type: 'webhook_outbox',
            resource_id: id,
            ip_address: req.clientIp ?? req.ip ?? undefined,
            user_agent: req.get('user-agent') ?? undefined,
            details: {
              adminId: req.admin?.id ?? null,
              adminEmail: req.admin?.email ?? null,
              previousStatus: current.status,
              previousAttempts: current.attempts,
              event: current.event,
              deliverySequence: current.delivery_sequence,
              reason: 'explicit_admin_redrive',
            } as Prisma.InputJsonObject,
          },
        });

        const row = await tx.webhookOutbox.findUnique({
          where: { id },
          select: SAFE_OUTBOX_SELECT,
        });

        return row ? { kind: 'redriven', row } : { kind: 'unavailable' };
      });

      if (result.kind === 'missing') {
        res.status(404).json(errorResponse('OUTBOX_NOT_FOUND', 'Webhook outbox row not found'));
        return;
      }
      if (result.kind === 'not_eligible') {
        res.status(409).json(errorResponse('OUTBOX_STATE_NOT_REDRIVABLE', 'Only due or dead terminal rows can be redriven'));
        return;
      }
      if (result.kind === 'conflict') {
        res.status(409).json(errorResponse('OUTBOX_STATE_CHANGED', 'Webhook outbox row changed before redrive'));
        return;
      }
      if (result.kind === 'unavailable') {
        res.status(503).json(errorResponse('OUTBOX_UNAVAILABLE', 'Webhook outbox is temporarily unavailable'));
        return;
      }

      res.status(202).json(successResponse({
        redriven: true,
        entry: toPublicOutbox(result.row),
      }));
    } catch (error) {
      handleOutboxError(error, req, res, 'Failed to redrive translation webhook outbox', id);
    }
  },
);

export default router;
