import type {
  TicketCategoryDashboard,
  TicketCategoryDashboardCreate,
  TicketCategoryDashboardUpdate,
  TicketConfigDashboard,
  TicketConfigDashboardPatch,
  TicketDashboard,
  TicketDashboardAction,
  TicketPriority,
  TicketStatus
} from '@nexumi/shared';
import type { Ticket, TicketCategory } from '@prisma/client';
import { prisma } from '../prisma';
import { addJobAndAwait, getTicketsQueue, getTicketsQueueEvents } from '../queues';

export class TicketPanelSyncError extends Error {
  constructor(
    message: string,
    public readonly code: 'timeout' | 'not_posted' | 'no_categories' = 'not_posted'
  ) {
    super(message);
    this.name = 'TicketPanelSyncError';
  }
}

export class TicketActionSyncError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'TicketActionSyncError';
  }
}

async function ensureTicketConfig(guildId: string) {
  await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
  return prisma.ticketConfig.upsert({ where: { guildId }, update: {}, create: { guildId } });
}

function toConfigDashboard(config: Awaited<ReturnType<typeof ensureTicketConfig>>): TicketConfigDashboard {
  return {
    enabled: config.enabled,
    mode: config.mode as TicketConfigDashboard['mode'],
    categoryId: config.categoryId ?? '',
    logChannelId: config.logChannelId ?? '',
    panelChannelId: config.panelChannelId ?? '',
    transcriptDm: config.transcriptDm,
    inactivityHours: config.inactivityHours,
    ratingEnabled: config.ratingEnabled
  };
}

export async function getTicketConfigDashboard(guildId: string): Promise<TicketConfigDashboard> {
  const config = await ensureTicketConfig(guildId);
  return toConfigDashboard(config);
}

/**
 * Dashboard has no discord.js Client, so panel posting is delegated to the bot
 * via the `tickets` BullMQ queue (`ticketPanelSync`).
 */
async function syncTicketPanel(guildId: string): Promise<void> {
  const categoryCount = await prisma.ticketCategory.count({ where: { guildId } });
  if (categoryCount === 0) {
    throw new TicketPanelSyncError(
      'Create at least one ticket category before posting the panel',
      'no_categories'
    );
  }

  const { confirmed, result } = await addJobAndAwait<{
    panelChannelId: string;
    panelMessageId: string;
  }>(
    getTicketsQueue(),
    getTicketsQueueEvents(),
    'ticketPanelSync',
    { guildId },
    {
      jobId: `ticket-panel-${guildId}-${Date.now()}`,
      removeOnComplete: true,
      removeOnFail: 25
    },
    30_000
  );

  if (!confirmed || !result?.panelMessageId) {
    throw new TicketPanelSyncError(
      confirmed
        ? 'Ticket panel job finished without posting a Discord message'
        : 'The bot did not confirm the ticket panel in time. It may still be posting — check the channel shortly.',
      confirmed ? 'not_posted' : 'timeout'
    );
  }

  const config = await prisma.ticketConfig.findUnique({ where: { guildId } });
  if (!config?.panelMessageId) {
    throw new TicketPanelSyncError(
      'Ticket panel job finished without storing a Discord message id',
      'not_posted'
    );
  }
}

async function maybeSyncTicketPanel(guildId: string): Promise<void> {
  const config = await prisma.ticketConfig.findUnique({ where: { guildId } });
  if (!config?.enabled || !config.panelChannelId) {
    return;
  }
  // Category CRUD must succeed even if Discord panel sync fails — otherwise
  // support roles / category fields appear "unsaved" in the UI while already
  // persisted in Postgres.
  try {
    await syncTicketPanel(guildId);
  } catch (error) {
    console.warn('[tickets] panel sync after category change failed', { guildId, error });
  }
}

export async function updateTicketConfigDashboard(
  guildId: string,
  patch: TicketConfigDashboardPatch
): Promise<TicketConfigDashboard> {
  await ensureTicketConfig(guildId);

  const { categoryId, logChannelId, panelChannelId, ...rest } = patch;
  const updated = await prisma.ticketConfig.update({
    where: { guildId },
    data: {
      ...rest,
      ...(categoryId !== undefined ? { categoryId: categoryId === '' ? null : categoryId } : {}),
      ...(logChannelId !== undefined ? { logChannelId: logChannelId === '' ? null : logChannelId } : {}),
      ...(panelChannelId !== undefined
        ? { panelChannelId: panelChannelId === '' ? null : panelChannelId }
        : {})
    }
  });
  const dashboard = toConfigDashboard(updated);

  if (dashboard.enabled && dashboard.panelChannelId) {
    await syncTicketPanel(guildId);
  }

  return dashboard;
}

function toCategoryDashboard(category: TicketCategory): TicketCategoryDashboard {
  return {
    id: category.id,
    name: category.name,
    description: category.description,
    emoji: category.emoji,
    supportRoleIds: category.supportRoleIds
  };
}

export async function listTicketCategories(guildId: string): Promise<TicketCategoryDashboard[]> {
  const categories = await prisma.ticketCategory.findMany({
    where: { guildId },
    orderBy: { createdAt: 'asc' }
  });
  return categories.map(toCategoryDashboard);
}

export class TicketCategoryConflictError extends Error {
  constructor() {
    super('A ticket category with this name already exists');
  }
}

export async function createTicketCategory(
  guildId: string,
  input: TicketCategoryDashboardCreate
): Promise<TicketCategoryDashboard> {
  await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
  try {
    const category = await prisma.ticketCategory.create({
      data: {
        guildId,
        name: input.name,
        description: input.description ?? null,
        emoji: input.emoji ?? null,
        supportRoleIds: input.supportRoleIds
      }
    });
    await maybeSyncTicketPanel(guildId);
    return toCategoryDashboard(category);
  } catch (error) {
    if (error && typeof error === 'object' && 'code' in error && error.code === 'P2002') {
      throw new TicketCategoryConflictError();
    }
    throw error;
  }
}

export async function updateTicketCategory(
  guildId: string,
  categoryId: string,
  patch: TicketCategoryDashboardUpdate
): Promise<TicketCategoryDashboard | null> {
  const existing = await prisma.ticketCategory.findFirst({ where: { id: categoryId, guildId } });
  if (!existing) {
    return null;
  }

  try {
    const updated = await prisma.ticketCategory.update({
      where: { id: categoryId },
      data: {
        ...(patch.name !== undefined ? { name: patch.name } : {}),
        ...(patch.description !== undefined ? { description: patch.description } : {}),
        ...(patch.emoji !== undefined ? { emoji: patch.emoji } : {}),
        ...(patch.supportRoleIds !== undefined ? { supportRoleIds: patch.supportRoleIds } : {})
      }
    });
    await maybeSyncTicketPanel(guildId);
    return toCategoryDashboard(updated);
  } catch (error) {
    if (error && typeof error === 'object' && 'code' in error && error.code === 'P2002') {
      throw new TicketCategoryConflictError();
    }
    throw error;
  }
}

export async function deleteTicketCategory(guildId: string, categoryId: string): Promise<boolean> {
  const result = await prisma.ticketCategory.deleteMany({ where: { id: categoryId, guildId } });
  if (result.count === 0) {
    return false;
  }
  const remaining = await prisma.ticketCategory.count({ where: { guildId } });
  if (remaining > 0) {
    await maybeSyncTicketPanel(guildId);
  }
  return true;
}

type TicketWithCategory = Ticket & { category: TicketCategory | null };

function toTicketDashboard(ticket: TicketWithCategory): TicketDashboard {
  return {
    id: ticket.id,
    openerId: ticket.openerId,
    claimedById: ticket.claimedById,
    channelId: ticket.channelId,
    threadId: ticket.threadId,
    status: ticket.status as TicketStatus,
    priority: ticket.priority as TicketPriority,
    subject: ticket.subject,
    categoryId: ticket.categoryId,
    categoryName: ticket.category?.name ?? null,
    lastActivityAt: ticket.lastActivityAt.toISOString(),
    createdAt: ticket.createdAt.toISOString()
  };
}

export async function listOpenTickets(guildId: string): Promise<TicketDashboard[]> {
  const tickets = await prisma.ticket.findMany({
    where: {
      guildId,
      status: { in: ['OPEN', 'CLAIMED'] }
    },
    include: { category: true },
    orderBy: { lastActivityAt: 'desc' },
    take: 100
  });
  return tickets.map(toTicketDashboard);
}

export async function getTicketDashboard(
  guildId: string,
  ticketId: string
): Promise<TicketDashboard | null> {
  const ticket = await prisma.ticket.findFirst({
    where: { id: ticketId, guildId },
    include: { category: true }
  });
  return ticket ? toTicketDashboard(ticket) : null;
}

/**
 * Close / claim / priority need Discord side effects → bot via BullMQ.
 */
export async function applyTicketDashboardAction(
  guildId: string,
  ticketId: string,
  action: TicketDashboardAction,
  actorUserId: string
): Promise<TicketDashboard | null> {
  const existing = await prisma.ticket.findFirst({
    where: { id: ticketId, guildId },
    include: { category: true }
  });
  if (!existing) {
    return null;
  }
  if (existing.status === 'CLOSED') {
    throw new TicketActionSyncError('This ticket is already closed');
  }

  const { confirmed } = await addJobAndAwait(
    getTicketsQueue(),
    getTicketsQueueEvents(),
    'ticketDashboardAction',
    {
      ticketId,
      guildId,
      actorUserId,
      action: action.action,
      reason: action.action === 'close' ? action.reason : undefined,
      priority: action.action === 'priority' ? action.priority : undefined
    },
    {
      jobId: `ticket-action-${ticketId}-${action.action}-${Date.now()}`,
      removeOnComplete: true,
      removeOnFail: 25
    },
    45_000
  );

  if (!confirmed) {
    throw new TicketActionSyncError(
      'The bot did not confirm the ticket action in time. Reload the list shortly to check the result.'
    );
  }

  if (action.action === 'close') {
    const closed = await getTicketDashboard(guildId, ticketId);
    return closed;
  }

  const updated = await getTicketDashboard(guildId, ticketId);
  if (!updated) {
    throw new TicketActionSyncError('Ticket action finished but the ticket could not be reloaded');
  }
  return updated;
}
