/**
 * CM-1: client groups (mailing/segment lists) service.
 *
 * CRUD for `client_groups` + membership management on `client_group_members`,
 * plus a member viewer that resolves each client's primary email for the
 * copy-emails feature. All reads/writes are port-scoped. Membership replace is
 * a wipe-and-rewrite transaction (same shape as setEntityTags).
 */

import { and, desc, eq, inArray, sql } from 'drizzle-orm';

import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
import { db } from '@/lib/db';
import { clientGroupMembers, clientGroups, clients } from '@/lib/db/schema';
import { withTransaction } from '@/lib/db/utils';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { syncGroupToMailchimp } from '@/lib/services/mailchimp.service';
import type {
  CreateClientGroupInput,
  UpdateClientGroupInput,
} from '@/lib/validators/client-groups';

export interface ClientGroupWithCount {
  id: string;
  name: string;
  description: string | null;
  color: string;
  mailchimpTag: string | null;
  memberCount: number;
  createdAt: Date;
  updatedAt: Date;
}

export interface GroupMember {
  clientId: string;
  fullName: string;
  email: string | null;
}

async function assertGroup(id: string, portId: string) {
  const group = await db.query.clientGroups.findFirst({
    where: and(eq(clientGroups.id, id), eq(clientGroups.portId, portId)),
  });
  if (!group || group.archivedAt) throw new NotFoundError('Client group not found');
  return group;
}

export async function listClientGroups(portId: string): Promise<ClientGroupWithCount[]> {
  const groups = await db
    .select()
    .from(clientGroups)
    .where(and(eq(clientGroups.portId, portId), sql`${clientGroups.archivedAt} IS NULL`))
    .orderBy(desc(clientGroups.createdAt));

  // Member counts in one grouped query (port-scoped).
  const counts = await db
    .select({ groupId: clientGroupMembers.groupId, n: sql<number>`count(*)::int` })
    .from(clientGroupMembers)
    .where(eq(clientGroupMembers.portId, portId))
    .groupBy(clientGroupMembers.groupId);
  const countMap = new Map(counts.map((c) => [c.groupId, c.n]));

  return groups.map((g) => ({
    id: g.id,
    name: g.name,
    description: g.description,
    color: g.color,
    mailchimpTag: g.mailchimpTag,
    memberCount: countMap.get(g.id) ?? 0,
    createdAt: g.createdAt,
    updatedAt: g.updatedAt,
  }));
}

export async function getClientGroupById(id: string, portId: string) {
  return assertGroup(id, portId);
}

export async function createClientGroup(
  portId: string,
  data: CreateClientGroupInput,
  meta: AuditMeta,
) {
  const [group] = await db
    .insert(clientGroups)
    .values({
      portId,
      name: data.name,
      description: data.description ?? null,
      color: data.color ?? '#6B7280',
      mailchimpTag: data.mailchimpTag ?? null,
    })
    .returning();
  if (!group) throw new ValidationError('Failed to create client group');
  void createAuditLog({
    ...meta,
    action: 'create',
    entityType: 'client_group',
    entityId: group.id,
    newValue: toAuditJson(group),
  });
  return group;
}

export async function updateClientGroup(
  id: string,
  portId: string,
  data: UpdateClientGroupInput,
  meta: AuditMeta,
) {
  await assertGroup(id, portId);
  const [updated] = await db
    .update(clientGroups)
    .set({
      ...(data.name !== undefined ? { name: data.name } : {}),
      ...(data.description !== undefined ? { description: data.description } : {}),
      ...(data.color !== undefined ? { color: data.color } : {}),
      ...(data.mailchimpTag !== undefined ? { mailchimpTag: data.mailchimpTag } : {}),
      updatedAt: new Date(),
    })
    .where(and(eq(clientGroups.id, id), eq(clientGroups.portId, portId)))
    .returning();
  if (!updated) throw new NotFoundError('Client group not found');
  void createAuditLog({
    ...meta,
    action: 'update',
    entityType: 'client_group',
    entityId: id,
    newValue: toAuditJson(data),
  });
  return updated;
}

export async function archiveClientGroup(id: string, portId: string, meta: AuditMeta) {
  await assertGroup(id, portId);
  await db
    .update(clientGroups)
    .set({ archivedAt: new Date(), updatedAt: new Date() })
    .where(and(eq(clientGroups.id, id), eq(clientGroups.portId, portId)));
  void createAuditLog({
    ...meta,
    action: 'archive',
    entityType: 'client_group',
    entityId: id,
  });
}

/** Members of a group, each with their primary email (for copy-emails). */
export async function listGroupMembers(groupId: string, portId: string): Promise<GroupMember[]> {
  await assertGroup(groupId, portId);
  const rows = await db
    .select({
      clientId: clients.id,
      fullName: clients.fullName,
      email: sql<string | null>`(
        SELECT cc.value FROM client_contacts cc
        WHERE cc.client_id = ${clients.id} AND cc.channel = 'email'
        ORDER BY cc.is_primary DESC
        LIMIT 1
      )`,
    })
    .from(clientGroupMembers)
    .innerJoin(clients, eq(clientGroupMembers.clientId, clients.id))
    .where(and(eq(clientGroupMembers.groupId, groupId), eq(clientGroupMembers.portId, portId)))
    .orderBy(clients.fullName);
  return rows;
}

/** Replace a group's membership with exactly `clientIds` (wipe-and-rewrite). */
export async function setGroupMembers(
  groupId: string,
  portId: string,
  clientIds: string[],
  meta: AuditMeta,
): Promise<void> {
  await assertGroup(groupId, portId);
  const unique = Array.from(new Set(clientIds));
  // Tenant-scope guard: every client must belong to this port.
  if (unique.length > 0) {
    const valid = await db
      .select({ id: clients.id })
      .from(clients)
      .where(and(inArray(clients.id, unique), eq(clients.portId, portId)));
    if (valid.length !== unique.length) {
      throw new ValidationError('One or more clients are not in this port');
    }
  }
  await withTransaction(async (tx) => {
    await tx.delete(clientGroupMembers).where(eq(clientGroupMembers.groupId, groupId));
    if (unique.length > 0) {
      await tx
        .insert(clientGroupMembers)
        .values(unique.map((clientId) => ({ groupId, clientId, portId })));
    }
  });
  void createAuditLog({
    ...meta,
    action: 'update',
    entityType: 'client_group_members',
    entityId: groupId,
    newValue: toAuditJson({ clientIds: unique }),
  });
  // CM-1 Mailchimp: fire-and-forget one-way push (inert until configured).
  void syncGroupToMailchimp(groupId, portId).catch(() => {});
}
