/**
 * Ticket workflows — state machine gated, transactional.
 *
 * Each workflow:
 *  (a) loads current ticket row
 *  (b) calls canTransitionTicket to gate transitions
 *  (c) updates ticket + inserts support_state_transitions in one transaction
 *  (d) inserts outbox notification rows in the same transaction
 *  (e) arms / re-arms SupportTicketDO alarm after commit
 *
 * Rate-limit defensive check is done inline per plan §Task 5 Step 3.
 * The API layer already runs rate-limit middleware; this is a second check.
 */

import { eq } from 'drizzle-orm';
import type { TxDrizzleClient } from '@/server/db/client.js';
import { type SupportOpenerRole } from '@/lib/enums/support-opener-role';
import { supportTickets, supportStateTransitions, agentDefinitions } from '@/server/db/schema.js';
import * as ticketMsgs from '@/server/db/queries/support-messages.js';
import * as ticketQ from '@/server/db/queries/support-tickets.js';
import * as attachmentsQ from '@/server/db/queries/support-attachments.js';
import { canTransitionTicket, type TicketState } from '@/server/support/state-machines.js';
import {
  enqueueSupportNotification,
  type SupportNotifContext,
} from '@/server/support/notifications.js';
import { enqueueOutbox } from '@/server/queues/outbox-producer.js';
import { insertOutboxRow } from '@/server/db/queries/outbox.js';

// ─── Types ─────────────────────────────────────────────────────────────────────

export type TicketCategory = 'account' | 'billing' | 'bug' | 'how_to' | 'complaint' | 'other';
export type TicketPriority = 'low' | 'normal' | 'high' | 'urgent';

export interface TicketWorkflowDeps {
  db: TxDrizzleClient;
  /** Abstracted DO arm — in tests inject a spy; in production call armSupportTicketAlarm. */
  armAlarm: (ticketId: string, at: Date) => Promise<void>;
  cryptoKey: string;
  config: {
    reopenLimit: number;
    slaHumanHours: number;
    autocloseDays: number;
  };
  /** Override for testing; defaults to Date.now(). */
  now?: () => Date;
  /** Site notification emails for ticket_reopened / escalated_to_human. */
  siteEmails?: string[];
  /** Base URL for generating ticket links (e.g. https://dev.multi.deal). */
  baseUrl?: string;
}

function getNow(deps: TicketWorkflowDeps): Date {
  return deps.now ? deps.now() : new Date();
}

function ticketUrl(deps: TicketWorkflowDeps, ticketId: string): string {
  return `${deps.baseUrl ?? ''}/support/tickets/${ticketId}`;
}

async function buildNotifCtx(
  deps: TicketWorkflowDeps,
  ticket: ticketQ.SupportTicketRow,
  extras: Partial<SupportNotifContext> = {},
): Promise<SupportNotifContext> {
  return {
    ticketId: ticket.id,
    openerId: ticket.openerId,
    openerLocale: 'he', // Phase 3: locale from ticket metadata or default he
    subject: ((ticket.metadata as Record<string, unknown>)?.subject as string) ?? '',
    category: ticket.category,
    ticketUrl: ticketUrl(deps, ticket.id),
    siteEmails: deps.siteEmails ?? [],
    ...extras,
  };
}

// ─── openTicket ────────────────────────────────────────────────────────────────

export async function openTicket(
  deps: TicketWorkflowDeps,
  input: {
    openerId: string;
    openerRole: SupportOpenerRole;
    category: TicketCategory;
    priority: TicketPriority;
    subject: string;
    body: string;
    relatedOrderLineId?: string;
    agentDefinitionSlug?: string;
    attachmentIds: string[];
    locale: 'he' | 'en';
  },
): Promise<{ ticketId: string }> {
  const now = getNow(deps);

  // Arm: SLA alarm fires at now + slaHumanHours
  const slaAt = new Date(now.getTime() + deps.config.slaHumanHours * 3600 * 1000);

  let ticketId!: string;
  let outboxIds: string[] = [];

  await deps.db.transaction(async (tx) => {
    let agentDefinitionId: string | null = null;
    if (input.agentDefinitionSlug) {
      const [agentDef] = await tx
        .select({ id: agentDefinitions.id })
        .from(agentDefinitions)
        .where(eq(agentDefinitions.slug, input.agentDefinitionSlug))
        .limit(1);
      agentDefinitionId = agentDef?.id ?? null;
    }

    // Insert ticket
    const [ticket] = await tx
      .insert(supportTickets)
      .values({
        openerId: input.openerId,
        openerRole: input.openerRole,
        category: input.category,
        priority: input.priority,
        status: 'open',
        relatedOrderLineId: input.relatedOrderLineId ?? null,
        agentDefinitionId,
        metadata: {
          subject: input.subject,
          slaHumanDueAt: slaAt.toISOString(),
        },
      })
      .returning();

    ticketId = ticket!.id;

    // Insert opening message
    await ticketMsgs.insertEncrypted(
      tx,
      {
        parentType: 'ticket',
        parentId: ticketId,
        authorType: input.openerRole,
        authorId: input.openerId,
        visibility: 'public',
      },
      input.body,
      deps.cryptoKey,
    );

    // Link attachments
    if (input.attachmentIds.length > 0) {
      await attachmentsQ.linkAttachmentsToParent(tx, input.attachmentIds, 'ticket', ticketId);
    }

    // Insert transition: (none) → open
    await tx.insert(supportStateTransitions).values({
      parentType: 'ticket',
      parentId: ticketId,
      fromState: 'open',
      toState: 'open',
      actorType: input.openerRole,
      actorId: input.openerId,
      reason: 'ticket_created',
      metadata: {},
    });

    // Enqueue notification
    const ctx = await buildNotifCtx(deps, ticket!, {
      subject: input.subject,
      openerLocale: input.locale,
    });
    outboxIds = await enqueueSupportNotification(tx, 'ticket_opened', ctx);

    // Enqueue AI dispatch (support agent runs asynchronously after commit)
    const aiRow = await insertOutboxRow(tx, {
      aggregateType: 'ticket',
      aggregateId: ticketId,
      eventType: 'support.ai.dispatch',
      payload: { parentType: 'ticket', parentId: ticketId, trigger: 'opened' },
    });
    outboxIds.push(aiRow.id);
  });

  // After tx commit: arm DO alarm + send outbox queue messages
  await deps.armAlarm(ticketId, slaAt);
  for (const id of outboxIds) {
    await enqueueOutbox(id);
  }

  return { ticketId };
}

// ─── postTicketMessage ─────────────────────────────────────────────────────────

export async function postTicketMessage(
  deps: TicketWorkflowDeps,
  input: {
    ticketId: string;
    authorId: string;
    authorType: SupportOpenerRole;
    body: string;
    attachmentIds: string[];
    locale: 'he' | 'en';
  },
): Promise<{ messageId: string; newStatus: TicketState }> {
  const now = getNow(deps);

  // Load current ticket
  const ticket = await ticketQ.findById(deps.db, input.ticketId);
  if (!ticket) throw new Error(`ticket_not_found:${input.ticketId}`);

  let newStatus: TicketState = ticket.status;
  let messageId!: string;
  let outboxIds: string[] = [];

  // If customer replies on resolved ticket → reopen
  const shouldReopen =
    input.authorType === 'customer' && (ticket.status === 'resolved' || ticket.status === 'closed');

  if (shouldReopen) {
    const check = canTransitionTicket(ticket.status, 'reopened', {
      actorType: 'customer',
      reopenCount: ticket.reopenCount,
      reopenLimit: deps.config.reopenLimit,
    });
    if (!check.ok) {
      throw new Error(check.reason);
    }
    newStatus = 'reopened';
  }

  await deps.db.transaction(async (tx) => {
    const msg = await ticketMsgs.insertEncrypted(
      tx,
      {
        parentType: 'ticket',
        parentId: input.ticketId,
        authorType: input.authorType,
        authorId: input.authorId,
        visibility: 'public',
      },
      input.body,
      deps.cryptoKey,
    );

    messageId = msg.id;

    if (input.attachmentIds.length > 0) {
      await attachmentsQ.linkAttachmentsToParent(tx, input.attachmentIds, 'ticket', input.ticketId);
    }

    if (shouldReopen) {
      await tx
        .update(supportTickets)
        .set({
          status: 'reopened',
          reopenCount: ticket.reopenCount + 1,
          updatedAt: now,
        })
        .where(eq(supportTickets.id, input.ticketId));

      await tx.insert(supportStateTransitions).values({
        parentType: 'ticket',
        parentId: input.ticketId,
        fromState: ticket.status,
        toState: 'reopened',
        actorType: 'customer',
        actorId: input.authorId,
        reason: 'customer_reply_reopen',
        metadata: {},
      });

      const ctx = await buildNotifCtx(deps, ticket, { openerLocale: input.locale });
      outboxIds = await enqueueSupportNotification(tx, 'ticket_reopened', ctx);
    } else {
      // Just update updatedAt for the message
      await tx
        .update(supportTickets)
        .set({ updatedAt: now })
        .where(eq(supportTickets.id, input.ticketId));
    }

    // If ticket is AI-handled and customer replies, trigger agent
    if (ticket.status === 'ai_handling' && input.authorType === 'customer') {
      const aiRow = await insertOutboxRow(tx, {
        aggregateType: 'ticket',
        aggregateId: input.ticketId,
        eventType: 'support.ai.dispatch',
        payload: { parentType: 'ticket', parentId: input.ticketId, trigger: 'user_reply' },
      });
      outboxIds.push(aiRow.id);
    }
  });

  for (const id of outboxIds) {
    await enqueueOutbox(id);
  }

  return { messageId, newStatus };
}

// ─── closeTicket ──────────────────────────────────────────────────────────────

export async function closeTicket(
  deps: TicketWorkflowDeps,
  input: { ticketId: string; actorId: string; locale: 'he' | 'en' },
): Promise<void> {
  const now = getNow(deps);

  const ticket = await ticketQ.findById(deps.db, input.ticketId);
  if (!ticket) throw new Error(`ticket_not_found:${input.ticketId}`);

  const check = canTransitionTicket(ticket.status, 'closed', { actorType: 'customer' });
  if (!check.ok) throw new Error(check.reason);

  await deps.db.transaction(async (tx) => {
    await tx
      .update(supportTickets)
      .set({ status: 'closed', closedAt: now, updatedAt: now })
      .where(eq(supportTickets.id, input.ticketId));

    await tx.insert(supportStateTransitions).values({
      parentType: 'ticket',
      parentId: input.ticketId,
      fromState: ticket.status,
      toState: 'closed',
      actorType: 'customer',
      actorId: input.actorId,
      reason: 'customer_closed',
      metadata: {},
    });
  });
}

// ─── reopenTicket ─────────────────────────────────────────────────────────────

export async function reopenTicket(
  deps: TicketWorkflowDeps,
  input: { ticketId: string; actorId: string; locale: 'he' | 'en' },
): Promise<void> {
  const now = getNow(deps);

  const ticket = await ticketQ.findById(deps.db, input.ticketId);
  if (!ticket) throw new Error(`ticket_not_found:${input.ticketId}`);

  const check = canTransitionTicket(ticket.status, 'reopened', {
    actorType: 'customer',
    reopenCount: ticket.reopenCount,
    reopenLimit: deps.config.reopenLimit,
  });
  if (!check.ok) throw new Error(check.reason);

  let outboxIds: string[] = [];

  await deps.db.transaction(async (tx) => {
    await tx
      .update(supportTickets)
      .set({
        status: 'reopened',
        reopenCount: ticket.reopenCount + 1,
        updatedAt: now,
      })
      .where(eq(supportTickets.id, input.ticketId));

    await tx.insert(supportStateTransitions).values({
      parentType: 'ticket',
      parentId: input.ticketId,
      fromState: ticket.status,
      toState: 'reopened',
      actorType: 'customer',
      actorId: input.actorId,
      reason: 'explicit_reopen',
      metadata: {},
    });

    const ctx = await buildNotifCtx(deps, ticket, { openerLocale: input.locale });
    outboxIds = await enqueueSupportNotification(tx, 'ticket_reopened', ctx);
  });

  for (const id of outboxIds) {
    await enqueueOutbox(id);
  }
}

// ─── resolveTicket ────────────────────────────────────────────────────────────

export async function resolveTicket(
  deps: TicketWorkflowDeps,
  input: { ticketId: string; actorId: string; resolution: string; locale: 'he' | 'en' },
): Promise<void> {
  const now = getNow(deps);

  const ticket = await ticketQ.findById(deps.db, input.ticketId);
  if (!ticket) throw new Error(`ticket_not_found:${input.ticketId}`);

  const check = canTransitionTicket(ticket.status, 'resolved', { actorType: 'human_agent' });
  if (!check.ok) throw new Error(check.reason);

  // Autoclose alarm: now + autocloseDays
  const autocloseAt = new Date(now.getTime() + deps.config.autocloseDays * 86400 * 1000);
  let outboxIds: string[] = [];

  await deps.db.transaction(async (tx) => {
    await tx
      .update(supportTickets)
      .set({
        status: 'resolved',
        resolvedAt: now,
        updatedAt: now,
        metadata: {
          ...(ticket.metadata as Record<string, unknown>),
          resolution: input.resolution,
          autocloseAt: autocloseAt.toISOString(),
        },
      })
      .where(eq(supportTickets.id, input.ticketId));

    await tx.insert(supportStateTransitions).values({
      parentType: 'ticket',
      parentId: input.ticketId,
      fromState: ticket.status,
      toState: 'resolved',
      actorType: 'human_agent',
      actorId: input.actorId,
      reason: 'agent_resolved',
      metadata: {},
    });

    const ctx = await buildNotifCtx(deps, ticket, {
      openerLocale: input.locale,
      resolution: input.resolution,
    });
    outboxIds = await enqueueSupportNotification(tx, 'ticket_resolved', ctx);
  });

  // Arm autoclose alarm
  await deps.armAlarm(input.ticketId, autocloseAt);

  for (const id of outboxIds) {
    await enqueueOutbox(id);
  }
}

// ─── escalateTicket ───────────────────────────────────────────────────────────

export async function escalateTicket(
  deps: TicketWorkflowDeps,
  input: { ticketId: string; reason: string; locale: 'he' | 'en' },
): Promise<void> {
  const now = getNow(deps);

  const ticket = await ticketQ.findById(deps.db, input.ticketId);
  if (!ticket) throw new Error(`ticket_not_found:${input.ticketId}`);

  // Allow escalation from open → escalated → awaiting_agent in one call
  const fromState = ticket.status;
  let outboxIds: string[] = [];

  // Validate: must be in a state that can reach escalated or awaiting_agent
  const canEscalate =
    canTransitionTicket(ticket.status, 'escalated', { actorType: 'system' }).ok ||
    canTransitionTicket(ticket.status, 'awaiting_agent', { actorType: 'system' }).ok;

  if (!canEscalate) {
    throw new Error(`illegal_transition:${ticket.status}->escalated_or_awaiting_agent`);
  }

  await deps.db.transaction(async (tx) => {
    // Go through escalated first if needed
    if (ticket.status !== 'escalated' && ticket.status !== 'awaiting_agent') {
      await tx.insert(supportStateTransitions).values({
        parentType: 'ticket',
        parentId: input.ticketId,
        fromState,
        toState: 'escalated',
        actorType: 'system',
        actorId: null,
        reason: input.reason,
        metadata: {},
      });
    }

    await tx
      .update(supportTickets)
      .set({ status: 'awaiting_agent', updatedAt: now })
      .where(eq(supportTickets.id, input.ticketId));

    await tx.insert(supportStateTransitions).values({
      parentType: 'ticket',
      parentId: input.ticketId,
      fromState: ticket.status !== 'escalated' ? 'escalated' : ticket.status,
      toState: 'awaiting_agent',
      actorType: 'system',
      actorId: null,
      reason: input.reason,
      metadata: {},
    });

    const ctx = await buildNotifCtx(deps, ticket, {
      openerLocale: input.locale,
      reason: input.reason,
    });
    outboxIds = await enqueueSupportNotification(tx, 'escalated_to_human', ctx);
  });

  for (const id of outboxIds) {
    await enqueueOutbox(id);
  }
}
