import { Hono, type Context } from 'hono';
import { createHash } from 'node:crypto';
import { z } from 'zod';

import { getDb, pgAll, pgGet, pgRunReturning } from '@/shared/db';

const SERVICES = ['iMessage', 'SMS'] as const;
const STATUSES = ['pending', 'in_flight', 'sent', 'cancelled', 'failed'] as const;

type Service = (typeof SERVICES)[number];
type Status = (typeof STATUSES)[number];

interface ScheduledSendRow {
  id: string;
  fire_at: Date;
  to_handle: string;
  body: string;
  service: Service;
  intent_hash: string;
  tier: string | null;
  dependency_intent_hash: string | null;
  status: Status;
  created_at: Date;
  attempts: number;
  last_error: string | null;
  sent_at: Date | null;
  cancellation_reason: string | null;
}

interface SerializedRow {
  id: string;
  fireAt: string;
  toHandle: string;
  body: string;
  service: Service;
  intentHash: string;
  tier: string | null;
  dependencyIntentHash: string | null;
  status: Status;
  createdAt: string;
  attempts: number;
  lastError: string | null;
  sentAt: string | null;
  cancellationReason: string | null;
}

function serialize(row: ScheduledSendRow): SerializedRow {
  return {
    id: row.id,
    fireAt: row.fire_at.toISOString(),
    toHandle: row.to_handle,
    body: row.body,
    service: row.service,
    intentHash: row.intent_hash,
    tier: row.tier,
    dependencyIntentHash: row.dependency_intent_hash,
    status: row.status,
    createdAt: row.created_at.toISOString(),
    attempts: row.attempts,
    lastError: row.last_error,
    sentAt: row.sent_at ? row.sent_at.toISOString() : null,
    cancellationReason: row.cancellation_reason,
  };
}

function deriveIntentHash(toHandle: string, body: string, fireAtIso: string): string {
  return createHash('sha256')
    .update(`${toHandle}|${body}|${fireAtIso}`)
    .digest('hex')
    .slice(0, 40);
}

const handleSchema = z
  .string()
  .min(1)
  .refine((s) => /^\+[1-9]\d{6,14}$/u.test(s) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(s), {
    message: 'toHandle must be E.164 phone (e.g. +14155552671) or email',
  });

const fireAtSchema = z
  .string()
  .refine((s) => !Number.isNaN(new Date(s).getTime()), {
    message: 'fireAt must be a valid ISO 8601 timestamp',
  });

const enqueueSchema = z.object({
  toHandle: handleSchema,
  body: z.string().min(1).max(5000),
  fireAt: fireAtSchema,
  service: z.enum(SERVICES).optional(),
  tier: z.string().max(64).optional(),
  intentHash: z.string().min(8).max(64).optional(),
  dependencyIntentHash: z.string().min(8).max(64).optional(),
});

const cancelSchema = z
  .object({
    id: z.string().uuid().optional(),
    intentHash: z.string().min(8).max(64).optional(),
    reason: z.string().max(200).optional(),
  })
  .refine((v) => Boolean(v.id) || Boolean(v.intentHash), {
    message: 'cancel requires id or intentHash',
  });

const listSchema = z.object({
  toHandle: handleSchema.optional(),
  statuses: z.array(z.enum(STATUSES)).max(STATUSES.length).optional(),
  limit: z.coerce.number().int().min(1).max(200).optional(),
});

const badRequest = (c: Context, err: z.ZodError) =>
  c.json(
    {
      success: false,
      error: { code: 'validation_error', message: 'invalid request payload', issues: err.issues },
    },
    400,
  );

const serverError = (c: Context, code: string, err: unknown) =>
  c.json(
    {
      success: false,
      error: { code, message: err instanceof Error ? err.message : String(err) },
    },
    500,
  );

export const scheduledSendAdminRouter = new Hono()
  .post('/enqueue', async (c) => {
    try {
      const payload = enqueueSchema.parse(await c.req.json());

      const fireAt = new Date(payload.fireAt);
      const minLeadMs = 30_000;
      if (fireAt.getTime() < Date.now() + minLeadMs) {
        return c.json(
          {
            success: false,
            error: {
              code: 'fire_at_too_soon',
              message: `fireAt must be at least 30s in the future (got ${fireAt.toISOString()})`,
            },
          },
          400,
        );
      }

      const service: Service = payload.service ?? 'iMessage';
      const intentHash =
        payload.intentHash ?? deriveIntentHash(payload.toHandle, payload.body, fireAt.toISOString());

      const db = getDb();

      const inserted = await pgGet<ScheduledSendRow>(
        db,
        `INSERT INTO outreach.scheduled_send
           (fire_at, to_handle, body, service, intent_hash, tier, dependency_intent_hash)
         VALUES ($1, $2, $3, $4, $5, $6, $7)
         ON CONFLICT (intent_hash) DO NOTHING
         RETURNING id, fire_at, to_handle, body, service, intent_hash, tier,
                   dependency_intent_hash, status, created_at, attempts,
                   last_error, sent_at, cancellation_reason`,
        [
          fireAt,
          payload.toHandle,
          payload.body,
          service,
          intentHash,
          payload.tier ?? null,
          payload.dependencyIntentHash ?? null,
        ],
      );

      if (inserted) {
        return c.json({ success: true, data: serialize(inserted) });
      }

      const existing = await pgGet<ScheduledSendRow>(
        db,
        `SELECT id, fire_at, to_handle, body, service, intent_hash, tier,
                dependency_intent_hash, status, created_at, attempts,
                last_error, sent_at, cancellation_reason
           FROM outreach.scheduled_send
          WHERE intent_hash = $1
          LIMIT 1`,
        [intentHash],
      );
      if (!existing) {
        return serverError(c, 'enqueue_lost', new Error('intent_hash collision but row not found'));
      }
      return c.json({ success: true, data: serialize(existing), conflicted: true });
    } catch (err) {
      if (err instanceof z.ZodError) return badRequest(c, err);
      return serverError(c, 'enqueue_error', err);
    }
  })
  .get('/list', async (c) => {
    try {
      const rawStatuses = c.req.queries('status');
      const parsed = listSchema.parse({
        toHandle: c.req.query('toHandle'),
        statuses: rawStatuses && rawStatuses.length > 0 ? rawStatuses : undefined,
        limit: c.req.query('limit'),
      });

      const statuses: readonly Status[] =
        parsed.statuses && parsed.statuses.length > 0 ? parsed.statuses : ['pending', 'in_flight'];
      const limit = parsed.limit ?? 50;

      const params: unknown[] = [statuses];
      let where = `status = ANY($1::text[])`;
      if (parsed.toHandle) {
        params.push(parsed.toHandle);
        where += ` AND to_handle = $${params.length}`;
      }

      const rows = await pgAll<ScheduledSendRow>(
        getDb(),
        `SELECT id, fire_at, to_handle, body, service, intent_hash, tier,
                dependency_intent_hash, status, created_at, attempts,
                last_error, sent_at, cancellation_reason
           FROM outreach.scheduled_send
          WHERE ${where}
          ORDER BY fire_at ASC
          LIMIT ${limit}`,
        params,
      );

      return c.json({ success: true, data: rows.map(serialize) });
    } catch (err) {
      if (err instanceof z.ZodError) return badRequest(c, err);
      return serverError(c, 'list_error', err);
    }
  })
  .post('/cancel', async (c) => {
    try {
      const payload = cancelSchema.parse(await c.req.json());
      const reason = payload.reason ?? 'manual_cancel';

      const params: unknown[] = [reason];
      let predicate: string;
      if (payload.id) {
        params.push(payload.id);
        predicate = `id = $${params.length}::uuid`;
      } else {
        params.push(payload.intentHash);
        predicate = `intent_hash = $${params.length}`;
      }

      const updated = await pgRunReturning<ScheduledSendRow>(
        getDb(),
        `UPDATE outreach.scheduled_send
            SET status = 'cancelled',
                cancellation_reason = $1
          WHERE ${predicate}
            AND status IN ('pending', 'in_flight')
          RETURNING id, fire_at, to_handle, body, service, intent_hash, tier,
                    dependency_intent_hash, status, created_at, attempts,
                    last_error, sent_at, cancellation_reason`,
        params,
      );

      if (!updated) {
        return c.json(
          {
            success: false,
            error: {
              code: 'not_cancellable',
              message: `no pending/in_flight scheduled_send row matched (${
                payload.id ? `id=${payload.id}` : `intentHash=${payload.intentHash}`
              })`,
            },
          },
          404,
        );
      }

      return c.json({ success: true, data: serialize(updated) });
    } catch (err) {
      if (err instanceof z.ZodError) return badRequest(c, err);
      return serverError(c, 'cancel_error', err);
    }
  });
