import prisma from '../lib/prisma';
import { AppError } from '../lib/utils';

export async function findCustomerWorkerJob(customerId: string, workerProfileId: string) {
  const hired = await prisma.job.findFirst({
    where: {
      customerId,
      workerId: workerProfileId,
      deletedAt: null,
    },
    orderBy: { updatedAt: 'desc' },
    select: { id: true },
  });
  if (hired) return hired;

  const withQuote = await prisma.job.findFirst({
    where: {
      customerId,
      deletedAt: null,
      quotations: {
        some: {
          workerId: workerProfileId,
          status: { in: ['PENDING', 'ACCEPTED'] },
        },
      },
    },
    orderBy: { updatedAt: 'desc' },
    select: { id: true },
  });
  if (withQuote) return withQuote;

  const workerSkills = await prisma.workerSkill.findMany({
    where: { workerId: workerProfileId },
    select: { categoryId: true },
  });
  const categoryIds = workerSkills.map((s) => s.categoryId);
  if (categoryIds.length === 0) return null;

  return prisma.job.findFirst({
    where: {
      customerId,
      deletedAt: null,
      status: { in: ['OPEN', 'QUOTED'] },
      categoryId: { in: categoryIds },
    },
    orderBy: { updatedAt: 'desc' },
    select: { id: true },
  });
}

export async function ensureJobConversation(jobId: string) {
  return prisma.conversation.upsert({
    where: { jobId },
    create: { jobId },
    update: {},
  });
}

/** Directory chat: customer ↔ worker without a job post. */
export async function ensureDirectWorkerConversation(
  customerId: string,
  workerProfileId: string
) {
  const existing = await prisma.conversation.findFirst({
    where: {
      customerId,
      contextWorkerId: workerProfileId,
      jobId: null,
    },
  });
  if (existing) return existing;

  try {
    return await prisma.conversation.create({
      data: {
        customerId,
        contextWorkerId: workerProfileId,
      },
    });
  } catch {
    // Race on unique (customerId, contextWorkerId)
    const again = await prisma.conversation.findFirst({
      where: {
        customerId,
        contextWorkerId: workerProfileId,
        jobId: null,
      },
    });
    if (again) return again;
    throw new AppError('Could not start conversation', 500);
  }
}

type InboxConversation = {
  id: string;
  updatedAt: Date | string;
  unreadCount?: number;
  jobId?: string | null;
  customerId?: string | null;
  messages?: { createdAt: string | Date; senderId: string; content: string }[];
  otherParticipant?: { id: string } | null;
  contextWorker?: { user?: { id: string } | null } | null;
  job?: {
    id?: string;
    title?: string;
    customerId?: string | null;
    customer?: { id: string } | null;
    worker?: { user?: { id: string } | null } | null;
    quotations?: { worker?: { user?: { id: string } | null } }[];
  } | null;
};

export type MergedInboxConversation<T> = T & {
  relatedConversationIds: string[];
  jobCount: number;
};

/** Collapse duplicate threads for the same job (e.g. from a race on create). */
export function dedupeInboxConversations<T extends InboxConversation>(
  conversations: T[]
): T[] {
  const byJobId = new Map<string, T>();
  const unscoped: T[] = [];

  for (const conversation of conversations) {
    const jobId = conversation.jobId ?? conversation.job?.id;
    if (!jobId) {
      unscoped.push(conversation);
      continue;
    }

    const existing = byJobId.get(jobId);
    if (!existing) {
      byJobId.set(jobId, conversation);
      continue;
    }

    const newer =
      new Date(conversation.updatedAt).getTime() > new Date(existing.updatedAt).getTime()
        ? conversation
        : existing;
    const older = newer === conversation ? existing : conversation;
    byJobId.set(jobId, {
      ...newer,
      unreadCount: (newer.unreadCount ?? 0) + (older.unreadCount ?? 0),
    });
  }

  return [...byJobId.values(), ...unscoped].sort(
    (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
  );
}

function resolveWorkerUserId(conversation: InboxConversation): string | null {
  const job = conversation.job;
  if (job?.worker?.user?.id) return job.worker.user.id;
  for (const quotation of job?.quotations ?? []) {
    if (quotation.worker?.user?.id) return quotation.worker.user.id;
  }
  return conversation.contextWorker?.user?.id ?? conversation.otherParticipant?.id ?? null;
}

export function resolveParticipantPairKey(
  conversation: InboxConversation,
  viewerUserId: string
): string | null {
  const customerId = conversation.job?.customerId ?? conversation.customerId ?? null;
  if (!customerId) return null;

  if (customerId === viewerUserId) {
    const workerUserId = resolveWorkerUserId(conversation);
    if (!workerUserId) return null;
    return `pair:${customerId}:${workerUserId}`;
  }

  return `pair:${customerId}:${viewerUserId}`;
}

/** One inbox row per customer ↔ worker pair; merges unread counts and message previews. */
export function mergeInboxByParticipant<T extends InboxConversation>(
  conversations: T[],
  viewerUserId: string
): MergedInboxConversation<T>[] {
  const deduped = dedupeInboxConversations(conversations);
  const byPair = new Map<string, MergedInboxConversation<T>>();
  const solo: MergedInboxConversation<T>[] = [];

  for (const conversation of deduped) {
    const pairKey = resolveParticipantPairKey(conversation, viewerUserId);
    if (!pairKey) {
      solo.push({ ...conversation, relatedConversationIds: [conversation.id], jobCount: 1 });
      continue;
    }

    const existing = byPair.get(pairKey);
    if (!existing) {
      byPair.set(pairKey, {
        ...conversation,
        relatedConversationIds: [conversation.id],
        jobCount: 1,
      });
      continue;
    }

    const relatedConversationIds = [...existing.relatedConversationIds, conversation.id];
    const totalUnread = (existing.unreadCount ?? 0) + (conversation.unreadCount ?? 0);
    const conversationIsNewer =
      new Date(conversation.updatedAt).getTime() > new Date(existing.updatedAt).getTime();
    const latest = conversationIsNewer ? conversation : existing;

    const existingPreview = existing.messages?.[0];
    const conversationPreview = conversation.messages?.[0];
    let messages = latest.messages;
    if (existingPreview && conversationPreview) {
      const useConversationPreview =
        new Date(conversationPreview.createdAt).getTime() >
        new Date(existingPreview.createdAt).getTime();
      messages = useConversationPreview ? conversation.messages : existing.messages;
    }

    byPair.set(pairKey, {
      ...latest,
      messages: messages ?? latest.messages,
      unreadCount: totalUnread,
      relatedConversationIds,
      jobCount: relatedConversationIds.length,
    });
  }

  return [...byPair.values(), ...solo].sort(
    (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
  );
}

const participantJobSelect = {
  customerId: true,
  worker: { select: { user: { select: { id: true } } } },
  quotations: {
    where: { status: { in: ['PENDING', 'ACCEPTED'] as const } },
    select: { worker: { select: { user: { select: { id: true } } } } },
  },
};

export async function getRelatedConversationIds(
  conversationId: string,
  userId: string,
  workerProfileId?: string
): Promise<string[]> {
  const conversations = await prisma.conversation.findMany({
    where: conversationListWhere(userId, workerProfileId),
    select: {
      id: true,
      customerId: true,
      job: { select: participantJobSelect },
      contextWorker: { select: { user: { select: { id: true } } } },
    },
  });

  const seed = conversations.find((conversation) => conversation.id === conversationId);
  if (!seed) return [conversationId];

  const pairKey = resolveParticipantPairKey(seed as InboxConversation, userId);
  if (!pairKey) return [conversationId];

  return conversations
    .filter(
      (conversation) =>
        resolveParticipantPairKey(conversation as InboxConversation, userId) === pairKey
    )
    .map((conversation) => conversation.id);
}

export async function resolvePrimaryConversationId(conversationIds: string[]): Promise<string> {
  if (conversationIds.length === 0) {
    throw new AppError('Conversation not found', 404);
  }
  if (conversationIds.length === 1) return conversationIds[0];

  const primary = await prisma.conversation.findFirst({
    where: { id: { in: conversationIds } },
    orderBy: { updatedAt: 'desc' },
    select: { id: true },
  });
  return primary?.id ?? conversationIds[0];
}

/** Create missing job threads so inbox lists hires and active quotes. */
export async function syncMessagingConversations(userId: string, workerProfileId?: string) {
  const jobIds = new Set<string>();

  const customerJobs = await prisma.job.findMany({
    where: {
      customerId: userId,
      deletedAt: null,
      OR: [
        { workerId: { not: null } },
        { quotations: { some: { status: { in: ['PENDING', 'ACCEPTED'] } } } },
      ],
    },
    select: { id: true },
  });
  customerJobs.forEach((j) => jobIds.add(j.id));

  if (workerProfileId) {
    const workerJobs = await prisma.job.findMany({
      where: {
        deletedAt: null,
        OR: [
          { workerId: workerProfileId },
          { quotations: { some: { workerId: workerProfileId } } },
        ],
      },
      select: { id: true },
    });
    workerJobs.forEach((j) => jobIds.add(j.id));
  }

  await Promise.all([...jobIds].map((jobId) => ensureJobConversation(jobId)));
}

export async function getWorkerProfileId(userId: string) {
  const profile = await prisma.workerProfile.findUnique({
    where: { userId },
    select: { id: true },
  });
  return profile?.id;
}

export async function assertJobMessagingAccess(jobId: string, userId: string, role: string) {
  const job = await prisma.job.findFirst({
    where: { id: jobId, deletedAt: null },
    include: {
      quotations: { select: { workerId: true, worker: { select: { userId: true } } } },
      worker: { select: { userId: true } },
    },
  });

  if (!job) throw new AppError('Job not found', 404);

  if (role === 'ADMIN') return job;

  if (job.customerId === userId) return job;

  // Workers may inquire on open jobs before submitting a quote
  if (role === 'WORKER' && ['OPEN', 'QUOTED'].includes(job.status)) {
    return job;
  }

  const workerProfileId = await getWorkerProfileId(userId);
  if (workerProfileId) {
    const hasQuote = job.quotations.some((q) => q.workerId === workerProfileId);
    const isAssigned = job.workerId === workerProfileId;

    if (hasQuote || isAssigned) return job;
  }

  throw new AppError('Not authorized to access this conversation', 403);
}

export async function assertConversationAccess(conversationId: string, userId: string, role: string) {
  const conversation = await prisma.conversation.findUnique({
    where: { id: conversationId },
    include: {
      job: {
        include: {
          quotations: { select: { workerId: true, worker: { select: { userId: true } } } },
          worker: { select: { userId: true } },
          customer: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
        },
      },
      contextWorker: {
        select: {
          id: true,
          userId: true,
          user: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
        },
      },
      customer: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
    },
  });

  if (!conversation) throw new AppError('Conversation not found', 404);

  // Directory / direct chat (no job)
  if (!conversation.job) {
    if (role === 'ADMIN') return conversation;

    const isCustomer = conversation.customerId === userId;
    const isWorker = conversation.contextWorker?.userId === userId;
    if (isCustomer || isWorker) return conversation;

    throw new AppError('Not authorized to access this conversation', 403);
  }

  await assertJobMessagingAccess(conversation.job.id, userId, role);
  return conversation;
}

export function conversationListWhere(userId: string, workerProfileId?: string) {
  const or: object[] = [
    { job: { customerId: userId } },
    { customerId: userId },
  ];

  if (workerProfileId) {
    or.push(
      { job: { workerId: workerProfileId } },
      { job: { quotations: { some: { workerId: workerProfileId } } } },
      { contextWorkerId: workerProfileId },
      { messages: { some: { senderId: userId } } }
    );
  }

  return { OR: or };
}
