import {
  and,
  asc,
  count,
  desc,
  eq,
  ilike,
  inArray,
  isNull,
  or,
  type SQL
} from 'drizzle-orm';
import {
  crmCustomerContacts,
  crmCustomers,
  crmOpportunities,
  crmLeads,
  memberships,
  trAuditLogs,
  users
} from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { getUserBranches, validateBranchAccess } from '@/features/foundation/branch-scope/service';
import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service';
import { auditAction, listAuditLogs } from '@/features/foundation/audit-log/service';
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
import {
  buildCrmSecurityContext,
  canAccessCustomer,
  canAccessScopedCrmRecord,
  type CrmSecurityContext
} from '@/features/crm/security/server/service';
import type {
  CreateLeadFollowupInput,
  CreateLeadInput,
  LeadActivityRecord,
  LeadAssignableUserLookup,
  LeadBranchOption,
  LeadContactLookup,
  LeadCustomerLookup,
  LeadDetail,
  LeadFollowupSummary,
  LeadLinkedOpportunitySummary,
  LeadListFilters,
  LeadListResponse,
  LeadOption,
  LeadReferenceData,
  LeadSummary,
  UpdateLeadInput
} from '../types';

const LEAD_OPTION_CATEGORIES = {
  awareness: 'crm_lead_awareness',
  leadChannel: 'crm_lead_channel',
  status: 'crm_lead_status',
  followupStatus: 'crm_lead_followup_status',
  lostReason: 'crm_lead_lost_reason',
  productType: 'crm_product_type',
  priority: 'crm_priority'
} as const;

export type LeadAccessContext = CrmSecurityContext;

type LeadRow = typeof crmLeads.$inferSelect;

type LeadLookupMaps = {
  awarenessLabels: Map<string, string>;
  leadChannelLabels: Map<string, string>;
  statusLabels: Map<string, string>;
  followupStatusLabels: Map<string, string>;
  lostReasonLabels: Map<string, string>;
  productTypeLabels: Map<string, string>;
  priorityLabels: Map<string, string>;
  customerNames: Map<string, string>;
  customerOwnerIds: Map<string, string | null>;
  contactNames: Map<string, string>;
  userNames: Map<string, string>;
};

type LeadFollowupAuditPayload = {
  id: string;
  leadId: string;
  followupDate: string;
  followupStatus: string;
  note: string | null;
  nextFollowupDate: string | null;
  createdBy: string;
  createdAt: string;
};

function mapOption(
  option: Awaited<ReturnType<typeof getActiveOptionsByCategory>>[number]
): LeadOption {
  return {
    id: option.id,
    code: option.code,
    label: option.label,
    value: option.value
  };
}

async function listAssignableLeadUsers(organizationId: string): Promise<LeadAssignableUserLookup[]> {
  const rows = await db
    .select({
      userId: memberships.userId,
      membershipRole: memberships.role,
      businessRole: memberships.businessRole,
      name: users.name
    })
    .from(memberships)
    .innerJoin(users, eq(memberships.userId, users.id))
    .where(eq(memberships.organizationId, organizationId))
    .orderBy(asc(users.name));

  const uniqueRows = [...new Map(rows.map((row) => [row.userId, row])).values()];

  return uniqueRows
    .filter(
      (row) =>
        row.membershipRole === 'admin' ||
        ['sales_manager', 'sales', 'sales_support'].includes(row.businessRole)
    )
    .map((row) => ({
      id: row.userId,
      name: row.name,
      membershipRole: row.membershipRole === 'admin' ? 'admin' : 'user',
      businessRole: row.businessRole
    }));
}

async function listLeadCustomers(organizationId: string): Promise<LeadCustomerLookup[]> {
  const rows = await db
    .select({
      id: crmCustomers.id,
      code: crmCustomers.code,
      name: crmCustomers.name,
      branchId: crmCustomers.branchId,
      ownerUserId: crmCustomers.ownerUserId
    })
    .from(crmCustomers)
    .where(and(eq(crmCustomers.organizationId, organizationId), isNull(crmCustomers.deletedAt)))
    .orderBy(asc(crmCustomers.name));

  const ownerIds = [
    ...new Set(rows.map((row) => row.ownerUserId).filter((value): value is string => Boolean(value)))
  ];
  const owners = ownerIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, ownerIds))
    : [];
  const ownerMap = new Map(owners.map((row) => [row.id, row.name]));

  return rows.map((row) => ({
    id: row.id,
    code: row.code,
    name: row.name,
    branchId: row.branchId ?? null,
    ownerUserId: row.ownerUserId ?? null,
    ownerName: row.ownerUserId ? (ownerMap.get(row.ownerUserId) ?? null) : null
  }));
}

async function listLeadContacts(organizationId: string): Promise<LeadContactLookup[]> {
  const rows = await db
    .select({
      id: crmCustomerContacts.id,
      customerId: crmCustomerContacts.customerId,
      name: crmCustomerContacts.name,
      email: crmCustomerContacts.email,
      mobile: crmCustomerContacts.mobile,
      isPrimary: crmCustomerContacts.isPrimary
    })
    .from(crmCustomerContacts)
    .where(
      and(eq(crmCustomerContacts.organizationId, organizationId), isNull(crmCustomerContacts.deletedAt))
    )
    .orderBy(desc(crmCustomerContacts.isPrimary), asc(crmCustomerContacts.name));

  return rows.map((row) => ({
    id: row.id,
    customerId: row.customerId,
    name: row.name,
    email: row.email ?? null,
    mobile: row.mobile ?? null,
    isPrimary: row.isPrimary
  }));
}

async function listLinkedOpportunities(
  leadId: string,
  organizationId: string
): Promise<LeadLinkedOpportunitySummary[]> {
  const rows = await db
    .select({
      id: crmOpportunities.id,
      code: crmOpportunities.code,
      title: crmOpportunities.title,
      status: crmOpportunities.status,
      assignedToUserId: crmOpportunities.assignedToUserId,
      updatedAt: crmOpportunities.updatedAt
    })
    .from(crmOpportunities)
    .where(
      and(
        eq(crmOpportunities.organizationId, organizationId),
        eq(crmOpportunities.leadId, leadId),
        isNull(crmOpportunities.deletedAt)
      )
    )
    .orderBy(desc(crmOpportunities.updatedAt));

  const assigneeIds = [
    ...new Set(rows.map((row) => row.assignedToUserId).filter((value): value is string => Boolean(value)))
  ];
  const assigneeRows = assigneeIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, assigneeIds))
    : [];
  const assigneeMap = new Map(assigneeRows.map((row) => [row.id, row.name]));

  return rows.map((row) => ({
    id: row.id,
    code: row.code,
    title: row.title,
    status: row.status,
    assignedToUserId: row.assignedToUserId ?? null,
    assignedToName: row.assignedToUserId ? (assigneeMap.get(row.assignedToUserId) ?? null) : null,
    updatedAt: row.updatedAt.toISOString()
  }));
}

async function listLeadActivity(
  leadId: string,
  organizationId: string
): Promise<LeadActivityRecord[]> {
  const auditLogs = await listAuditLogs({
    organizationId,
    entityType: 'crm_lead',
    entityId: leadId,
    limit: 25
  });
  const userIds = [...new Set(auditLogs.map((log) => log.userId))];
  const actorRows = userIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, userIds))
    : [];
  const actorMap = new Map(actorRows.map((row) => [row.id, row.name]));

  return auditLogs.map((log) => ({
    id: log.id,
    action: log.action,
    entityType: log.entityType,
    entityId: log.entityId,
    createdAt: log.createdAt,
    userId: log.userId,
    actorName: actorMap.get(log.userId) ?? null,
    beforeData: log.beforeData,
    afterData: log.afterData,
    requestId: log.requestId
  }));
}

function mapBranch(
  branch: Awaited<ReturnType<typeof getUserBranches>>[number]
): LeadBranchOption {
  return {
    id: branch.id,
    code: branch.code,
    name: branch.name,
    value: branch.value
  };
}

function toIsoDateTime(value: Date | null | undefined) {
  return value ? value.toISOString() : null;
}

function normalizeLeadOutcome(value?: string | null): 'open' | 'won' | 'lost' {
  if (value === 'won' || value === 'lost') {
    return value;
  }

  return 'open';
}

function mapLeadSummary(row: LeadRow, lookups: LeadLookupMaps): LeadSummary {
  return {
    id: row.id,
    organizationId: row.organizationId,
    branchId: row.branchId ?? null,
    code: row.code,
    customerId: row.customerId ?? null,
    customerName: row.customerId ? (lookups.customerNames.get(row.customerId) ?? null) : null,
    contactId: row.contactId ?? null,
    contactName: row.contactId ? (lookups.contactNames.get(row.contactId) ?? null) : null,
    description: row.description ?? null,
    projectName: row.projectName ?? null,
    projectLocation: row.projectLocation ?? null,
    leadChannel: row.leadChannel ?? null,
    leadChannelLabel: row.leadChannel
      ? (lookups.leadChannelLabels.get(row.leadChannel) ?? null)
      : null,
    productType: row.productType ?? null,
    priority: row.priority ?? null,
    estimatedValue: row.estimatedValue ?? null,
    awarenessId: row.awarenessId ?? null,
    awarenessLabel: row.awarenessId ? (lookups.awarenessLabels.get(row.awarenessId) ?? null) : null,
    status: row.status,
    statusLabel: lookups.statusLabels.get(row.status) ?? null,
    followupStatus: row.followupStatus ?? null,
    followupStatusLabel: row.followupStatus
      ? (lookups.followupStatusLabels.get(row.followupStatus) ?? null)
      : null,
    lostReason: row.lostReason ?? null,
    lostReasonLabel: row.lostReason ? (lookups.lostReasonLabels.get(row.lostReason) ?? null) : null,
    outcome: row.outcome,
    ownerMarketingUserId: row.ownerMarketingUserId ?? null,
    ownerMarketingName: row.ownerMarketingUserId
      ? (lookups.userNames.get(row.ownerMarketingUserId) ?? null)
      : null,
    assignedSalesOwnerId: row.assignedSalesOwnerId ?? null,
    assignedSalesOwnerName: row.assignedSalesOwnerId
      ? (lookups.userNames.get(row.assignedSalesOwnerId) ?? null)
      : null,
    suggestedSalesOwnerId: row.customerId ? (lookups.customerOwnerIds.get(row.customerId) ?? null) : null,
    suggestedSalesOwnerName:
      row.customerId && lookups.customerOwnerIds.get(row.customerId)
        ? (lookups.userNames.get(lookups.customerOwnerIds.get(row.customerId) ?? '') ?? null)
        : null,
    assignedAt: toIsoDateTime(row.assignedAt),
    assignedBy: row.assignedBy ?? null,
    assignedByName: row.assignedBy ? (lookups.userNames.get(row.assignedBy) ?? null) : null,
    assignmentRemark: row.assignmentRemark ?? null,
    createdBy: row.createdBy,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: toIsoDateTime(row.deletedAt)
  };
}

function mapLeadDetail(
  row: LeadRow,
  lookups: LeadLookupMaps,
  suggestedSalesOwnerId: string | null,
  relatedOpportunityCount: number
): LeadDetail {
  return {
    ...mapLeadSummary(row, lookups),
    suggestedSalesOwnerId,
    relatedOpportunityCount,
    linkedOpportunities: [],
    activity: []
  };
}

function parseSort(sort?: string) {
  if (!sort) {
    return desc(crmLeads.updatedAt);
  }

  try {
    const parsed = JSON.parse(sort) as Array<{ id: string; desc?: boolean }>;
    const [rule] = parsed;

    if (!rule) {
      return desc(crmLeads.updatedAt);
    }

    const sortMap = {
      code: crmLeads.code,
      projectName: crmLeads.projectName,
      status: crmLeads.status,
      followupStatus: crmLeads.followupStatus,
      outcome: crmLeads.outcome,
      updatedAt: crmLeads.updatedAt,
      createdAt: crmLeads.createdAt
    } as const;

    const column = sortMap[rule.id as keyof typeof sortMap];

    if (!column) {
      return desc(crmLeads.updatedAt);
    }

    return rule.desc ? desc(column) : asc(column);
  } catch {
    return desc(crmLeads.updatedAt);
  }
}

async function resolveValidOptionIds(category: string, organizationId: string) {
  const options = await getActiveOptionsByCategory(category, { organizationId });
  return new Set(options.map((option) => option.id));
}

async function resolveOptionIdByCode(
  organizationId: string,
  category: string,
  code: string
) {
  const options = await getActiveOptionsByCategory(category, { organizationId });
  return options.find((option) => option.code === code)?.id ?? null;
}

async function assertMasterOptionValue(
  organizationId: string,
  category: string,
  optionId?: string | null
) {
  if (!optionId) {
    return;
  }

  const validIds = await resolveValidOptionIds(category, organizationId);

  if (!validIds.has(optionId)) {
    throw new AuthError(`Invalid option for ${category}`, 400);
  }
}

async function assertCustomerBelongsToOrganization(id: string, organizationId: string) {
  const [customer] = await db
    .select()
    .from(crmCustomers)
    .where(
      and(
        eq(crmCustomers.id, id),
        eq(crmCustomers.organizationId, organizationId),
        isNull(crmCustomers.deletedAt)
      )
    )
    .limit(1);

  if (!customer) {
    throw new AuthError('Customer not found', 404);
  }

  return customer;
}

async function assertContactBelongsToOrganization(
  contactId: string,
  organizationId: string,
  customerId?: string | null
) {
  const [contact] = await db
    .select()
    .from(crmCustomerContacts)
    .where(
      and(
        eq(crmCustomerContacts.id, contactId),
        eq(crmCustomerContacts.organizationId, organizationId),
        isNull(crmCustomerContacts.deletedAt),
        ...(customerId ? [eq(crmCustomerContacts.customerId, customerId)] : [])
      )
    )
    .limit(1);

  if (!contact) {
    throw new AuthError('Contact not found', 404);
  }

  return contact;
}

async function assertMembershipUserBelongsToOrganization(
  userId: string,
  organizationId: string
) {
  const [membership] = await db
    .select({ userId: memberships.userId })
    .from(memberships)
    .where(
      and(
        eq(memberships.userId, userId),
        eq(memberships.organizationId, organizationId)
      )
    )
    .limit(1);

  if (!membership) {
    throw new AuthError('Assigned user not found in organization', 404);
  }
}

function canAccessLeadRow(row: LeadRow, accessContext: LeadAccessContext) {
  return canAccessScopedCrmRecord(accessContext, {
    branchId: row.branchId,
    createdBy: row.createdBy,
    ownerUserId: row.ownerMarketingUserId
  });
}

function buildAccessScopedFilters(context: LeadAccessContext): SQL[] {
  const filters: SQL[] = [];

  if (context.branchScopeMode === 'assigned' && context.branchScopeIds.length > 0) {
    filters.push(inArray(crmLeads.branchId, context.branchScopeIds));
  }

  if (context.branchScopeMode === 'none') {
    filters.push(eq(crmLeads.id, '__forbidden__'));
  }

  if (context.membershipRole !== 'admin' && context.ownershipScope === 'own') {
    filters.push(
      or(
        eq(crmLeads.createdBy, context.userId),
        eq(crmLeads.ownerMarketingUserId, context.userId)
      )!
    );
  }

  return filters;
}

async function assertLeadBelongsToOrganization(
  id: string,
  organizationId: string,
  accessContext?: LeadAccessContext
) {
  const [lead] = await db
    .select()
    .from(crmLeads)
    .where(
      and(
        eq(crmLeads.id, id),
        eq(crmLeads.organizationId, organizationId),
        isNull(crmLeads.deletedAt)
      )
    )
    .limit(1);

  if (!lead) {
    throw new AuthError('Lead not found', 404);
  }

  if (accessContext && !canAccessLeadRow(lead, accessContext)) {
    throw new AuthError('Forbidden', 403);
  }

  return lead;
}

async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise<LeadLookupMaps> {
  const [awarenesses, leadChannels, statuses, followupStatuses, lostReasons, productTypes, priorities] =
    await Promise.all([
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.leadChannel, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.productType, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.priority, { organizationId })
    ]);
  const customerIds = [
    ...new Set(rows.map((row) => row.customerId).filter((value): value is string => Boolean(value)))
  ];
  const contactIds = [
    ...new Set(rows.map((row) => row.contactId).filter((value): value is string => Boolean(value)))
  ];
  const userIds = [
    ...new Set(
      rows
        .flatMap((row) => [
          row.ownerMarketingUserId,
          row.assignedSalesOwnerId,
          row.assignedBy,
          row.createdBy
        ])
        .filter((value): value is string => Boolean(value))
    )
  ];

  const [customerRows, contactRows] = await Promise.all([
    customerIds.length
      ? db
          .select({ id: crmCustomers.id, name: crmCustomers.name, ownerUserId: crmCustomers.ownerUserId })
          .from(crmCustomers)
          .where(inArray(crmCustomers.id, customerIds))
      : Promise.resolve([]),
    contactIds.length
      ? db
          .select({ id: crmCustomerContacts.id, name: crmCustomerContacts.name })
          .from(crmCustomerContacts)
          .where(inArray(crmCustomerContacts.id, contactIds))
      : Promise.resolve([]),
  ]);
  const resolvedUserIds = [
    ...new Set(
      userIds.concat(
        customerRows
          .map((item) => item.ownerUserId)
          .filter((value): value is string => Boolean(value))
      )
    )
  ];
  const userRows = resolvedUserIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, resolvedUserIds))
    : [];

  return {
    awarenessLabels: new Map(awarenesses.map((item) => [item.id, item.label])),
    leadChannelLabels: new Map(leadChannels.map((item) => [item.id, item.label])),
    statusLabels: new Map(statuses.map((item) => [item.id, item.label])),
    followupStatusLabels: new Map(followupStatuses.map((item) => [item.id, item.label])),
    lostReasonLabels: new Map(lostReasons.map((item) => [item.id, item.label])),
    productTypeLabels: new Map(productTypes.map((item) => [item.id, item.label])),
    priorityLabels: new Map(priorities.map((item) => [item.id, item.label])),
    customerNames: new Map(customerRows.map((item) => [item.id, item.name])),
    customerOwnerIds: new Map(customerRows.map((item) => [item.id, item.ownerUserId ?? null])),
    contactNames: new Map(contactRows.map((item) => [item.id, item.name])),
    userNames: new Map(userRows.map((item) => [item.id, item.name]))
  };
}

async function validateLeadPayload(
  organizationId: string,
  userId: string,
  payload: CreateLeadInput | UpdateLeadInput,
  accessContext?: LeadAccessContext
) {
  await Promise.all([
    assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.awareness, payload.awarenessId),
    assertMasterOptionValue(
      organizationId,
      LEAD_OPTION_CATEGORIES.leadChannel,
      payload.leadChannel
    ),
    assertMasterOptionValue(
      organizationId,
      LEAD_OPTION_CATEGORIES.productType,
      payload.productType
    ),
    assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.priority, payload.priority),
    payload.status
      ? assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.status, payload.status)
      : Promise.resolve(),
    assertMasterOptionValue(
      organizationId,
      LEAD_OPTION_CATEGORIES.followupStatus,
      payload.followupStatus
    ),
    assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.lostReason, payload.lostReason)
  ]);

  if (payload.branchId) {
    await validateBranchAccess(payload.branchId);
  }

  if (payload.customerId) {
    const customer = await assertCustomerBelongsToOrganization(payload.customerId, organizationId);

    if (accessContext && !canAccessCustomer(accessContext, customer)) {
      throw new AuthError('Forbidden', 403);
    }
  }

  if (payload.contactId) {
    await assertContactBelongsToOrganization(
      payload.contactId,
      organizationId,
      payload.customerId ?? undefined
    );
  }

  if (payload.ownerMarketingUserId) {
    await assertMembershipUserBelongsToOrganization(payload.ownerMarketingUserId, organizationId);
  }

  if (accessContext) {
    const draftRow = {
      branchId: payload.branchId ?? null,
      createdBy: userId,
      ownerMarketingUserId: payload.ownerMarketingUserId ?? userId
    };

    if (
      !canAccessScopedCrmRecord(accessContext, {
        branchId: draftRow.branchId,
        createdBy: draftRow.createdBy,
        ownerUserId: draftRow.ownerMarketingUserId
      })
    ) {
      throw new AuthError('Forbidden', 403);
    }
  }
}

export async function getLeadReferenceData(organizationId: string): Promise<LeadReferenceData> {
  const [
    awarenesses,
    leadChannels,
    statuses,
    followupStatuses,
    lostReasons,
    productTypes,
    priorities,
    branches,
    customers,
    contacts,
    assignableUsers
  ] =
    await Promise.all([
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.leadChannel, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.productType, { organizationId }),
      getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.priority, { organizationId }),
      getUserBranches(),
      listLeadCustomers(organizationId),
      listLeadContacts(organizationId),
      listAssignableLeadUsers(organizationId)
    ]);

  return {
    awarenesses: awarenesses.map(mapOption),
    leadChannels: leadChannels.map(mapOption),
    statuses: statuses.map(mapOption),
    followupStatuses: followupStatuses.map(mapOption),
    lostReasons: lostReasons.map(mapOption),
    productTypes: productTypes.map(mapOption),
    priorities: priorities.map(mapOption),
    branches: branches.map(mapBranch),
    customers,
    contacts,
    assignableUsers
  };
}

export async function listLeads(
  organizationId: string,
  filters: LeadListFilters = {},
  accessContext?: LeadAccessContext
): Promise<{ items: LeadSummary[]; totalItems: number }> {
  const page = filters.page ?? 1;
  const limit = filters.limit ?? 10;
  const offset = (page - 1) * limit;
  const whereFilters: SQL[] = [
    eq(crmLeads.organizationId, organizationId),
    isNull(crmLeads.deletedAt),
    ...(filters.branchId ? [eq(crmLeads.branchId, filters.branchId)] : []),
    ...(filters.customerId ? [eq(crmLeads.customerId, filters.customerId)] : []),
    ...(filters.status ? [eq(crmLeads.status, filters.status)] : []),
    ...(filters.awarenessId ? [eq(crmLeads.awarenessId, filters.awarenessId)] : []),
    ...(filters.followupStatus ? [eq(crmLeads.followupStatus, filters.followupStatus)] : []),
    ...(filters.outcome ? [eq(crmLeads.outcome, filters.outcome)] : []),
    ...(filters.ownerMarketingUserId
      ? [eq(crmLeads.ownerMarketingUserId, filters.ownerMarketingUserId)]
      : []),
    ...(filters.search
      ? [
          or(
            ilike(crmLeads.code, `%${filters.search}%`),
            ilike(crmLeads.projectName, `%${filters.search}%`),
            ilike(crmLeads.projectLocation, `%${filters.search}%`)
          )!
        ]
      : []),
    ...(accessContext ? buildAccessScopedFilters(accessContext) : [])
  ];
  const where = and(...whereFilters);
  const [totalResult] = await db.select({ value: count() }).from(crmLeads).where(where);
  const rows = await db
    .select()
    .from(crmLeads)
    .where(where)
    .orderBy(parseSort(filters.sort))
    .limit(limit)
    .offset(offset);
  const lookups = await buildLookupMaps(organizationId, rows);

  return {
    items: rows.map((row) => mapLeadSummary(row, lookups)),
    totalItems: totalResult?.value ?? 0
  };
}

export async function getLeadById(
  id: string,
  organizationId: string,
  accessContext?: LeadAccessContext
): Promise<LeadDetail> {
  const lead = await assertLeadBelongsToOrganization(id, organizationId, accessContext);
  const [lookups, relatedOpportunityResult, customer, linkedOpportunities, activity] = await Promise.all([
    buildLookupMaps(organizationId, [lead]),
    db
      .select({ value: count() })
      .from(crmOpportunities)
      .where(
        and(
          eq(crmOpportunities.organizationId, organizationId),
          eq(crmOpportunities.leadId, id),
          isNull(crmOpportunities.deletedAt)
        )
      ),
    lead.customerId ? assertCustomerBelongsToOrganization(lead.customerId, organizationId) : Promise.resolve(null),
    listLinkedOpportunities(id, organizationId),
    listLeadActivity(id, organizationId)
  ]);
  const detail = mapLeadDetail(
    lead,
    lookups,
    customer?.ownerUserId ?? null,
    relatedOpportunityResult[0]?.value ?? 0
  );

  return {
    ...detail,
    linkedOpportunities,
    activity
  };
}

export async function createLead(
  organizationId: string,
  userId: string,
  payload: CreateLeadInput,
  accessContext?: LeadAccessContext
): Promise<LeadDetail> {
  await validateLeadPayload(organizationId, userId, payload, accessContext);
  const defaultStatusId =
    (await resolveOptionIdByCode(organizationId, LEAD_OPTION_CATEGORIES.status, 'new_job')) ??
    payload.status;

  if (!defaultStatusId) {
    throw new AuthError('Lead status option new_job is not configured', 400);
  }

  const codeResult = await generateNextDocumentCode({
    organizationId,
    documentType: 'crm_lead',
    branchId: payload.branchId ?? null
  });

  const [created] = await db
    .insert(crmLeads)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      branchId: payload.branchId ?? null,
      code: codeResult.code,
      customerId: payload.customerId ?? null,
      contactId: payload.contactId ?? null,
      description: payload.description?.trim() || null,
      projectName: payload.projectName?.trim() || null,
      projectLocation: payload.projectLocation?.trim() || null,
      leadChannel: payload.leadChannel ?? null,
      productType: payload.productType ?? null,
      priority: payload.priority ?? null,
      estimatedValue: payload.estimatedValue ?? null,
      awarenessId: payload.awarenessId ?? null,
      status: defaultStatusId,
      followupStatus: payload.followupStatus ?? null,
      lostReason: null,
      outcome: 'open',
      ownerMarketingUserId: userId,
      createdBy: userId
    })
    .returning();

  return getLeadById(created.id, organizationId, accessContext);
}

export async function updateLead(
  id: string,
  organizationId: string,
  userId: string,
  payload: UpdateLeadInput,
  accessContext?: LeadAccessContext
): Promise<LeadDetail> {
  const current = await assertLeadBelongsToOrganization(id, organizationId, accessContext);
  const nextPayload = {
    branchId: payload.branchId === undefined ? current.branchId : payload.branchId,
    customerId: payload.customerId === undefined ? current.customerId : payload.customerId,
    contactId:
      payload.contactId === undefined
        ? current.contactId
        : payload.contactId,
    description: payload.description === undefined ? current.description : payload.description,
    projectName: payload.projectName === undefined ? current.projectName : payload.projectName,
    projectLocation:
      payload.projectLocation === undefined ? current.projectLocation : payload.projectLocation,
    leadChannel: payload.leadChannel === undefined ? current.leadChannel : payload.leadChannel,
    productType: payload.productType === undefined ? current.productType : payload.productType,
    priority: payload.priority === undefined ? current.priority : payload.priority,
    estimatedValue:
      payload.estimatedValue === undefined ? current.estimatedValue : payload.estimatedValue,
    awarenessId: payload.awarenessId === undefined ? current.awarenessId : payload.awarenessId,
    status: payload.status ?? current.status,
    followupStatus:
      payload.followupStatus === undefined ? current.followupStatus : payload.followupStatus,
    lostReason: payload.lostReason === undefined ? current.lostReason : payload.lostReason,
    outcome: payload.outcome ?? normalizeLeadOutcome(current.outcome),
    ownerMarketingUserId:
      payload.ownerMarketingUserId === undefined
        ? current.ownerMarketingUserId
        : payload.ownerMarketingUserId
  } satisfies CreateLeadInput;

  await validateLeadPayload(organizationId, userId, nextPayload, accessContext);

  await db
    .update(crmLeads)
    .set({
      branchId: nextPayload.branchId ?? null,
      customerId: nextPayload.customerId ?? null,
      contactId: nextPayload.contactId ?? null,
      description: nextPayload.description?.trim() || null,
      projectName: nextPayload.projectName?.trim() || null,
      projectLocation: nextPayload.projectLocation?.trim() || null,
      leadChannel: nextPayload.leadChannel ?? null,
      productType: nextPayload.productType ?? null,
      priority: nextPayload.priority ?? null,
      estimatedValue: nextPayload.estimatedValue ?? null,
      awarenessId: nextPayload.awarenessId ?? null,
      status: nextPayload.status,
      followupStatus: nextPayload.followupStatus ?? null,
      lostReason: nextPayload.lostReason ?? null,
      outcome: normalizeLeadOutcome(nextPayload.outcome),
      ownerMarketingUserId: nextPayload.ownerMarketingUserId ?? null,
      updatedAt: new Date()
    })
    .where(eq(crmLeads.id, id));

  return getLeadById(id, organizationId, accessContext);
}

export async function deleteLead(
  id: string,
  organizationId: string,
  accessContext?: LeadAccessContext
): Promise<LeadDetail> {
  await assertLeadBelongsToOrganization(id, organizationId, accessContext);
  const now = new Date();

  await db
    .update(crmLeads)
    .set({
      deletedAt: now,
      updatedAt: now
    })
    .where(eq(crmLeads.id, id));

  return getLeadById(id, organizationId, accessContext).catch(async () => {
    const [deleted] = await db
      .select()
      .from(crmLeads)
      .where(eq(crmLeads.id, id))
      .limit(1);

    const lookups = await buildLookupMaps(organizationId, deleted ? [deleted] : []);

    return {
      ...mapLeadDetail(deleted!, lookups, null, 0),
      linkedOpportunities: [],
      activity: []
    };
  });
}

export async function listLeadFollowups(
  leadId: string,
  organizationId: string,
  accessContext?: LeadAccessContext
): Promise<LeadFollowupSummary[]> {
  await assertLeadBelongsToOrganization(leadId, organizationId, accessContext);
  const [followupStatuses, rows] = await Promise.all([
    getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
    db
      .select()
      .from(trAuditLogs)
      .where(
        and(
          eq(trAuditLogs.organizationId, organizationId),
          eq(trAuditLogs.entityType, 'crm_lead'),
          eq(trAuditLogs.entityId, leadId),
          eq(trAuditLogs.action, 'create_lead_followup')
        )
      )
      .orderBy(desc(trAuditLogs.createdAt))
  ]);
  const userIds = [
    ...new Set(rows.map((row) => row.userId).filter((value): value is string => Boolean(value)))
  ];
  const userRows = userIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, userIds))
    : [];
  const userNames = new Map(userRows.map((row) => [row.id, row.name]));
  const followupStatusLabels = new Map(followupStatuses.map((item) => [item.id, item.label]));

  return rows
    .map((row) => {
      const payload = row.afterData as LeadFollowupAuditPayload | null;

      if (!payload || typeof payload !== 'object') {
        return null;
      }

      return {
        id: payload.id,
        leadId: payload.leadId,
        followupDate: payload.followupDate,
        followupStatus: payload.followupStatus,
        followupStatusLabel: followupStatusLabels.get(payload.followupStatus) ?? null,
        note: payload.note ?? null,
        nextFollowupDate: payload.nextFollowupDate ?? null,
        createdAt: payload.createdAt ?? row.createdAt.toISOString(),
        createdBy: payload.createdBy ?? row.userId,
        createdByName: userNames.get(payload.createdBy ?? row.userId) ?? null
      } satisfies LeadFollowupSummary;
    })
    .filter((item): item is LeadFollowupSummary => Boolean(item));
}

export async function createLeadFollowup(
  leadId: string,
  organizationId: string,
  userId: string,
  payload: CreateLeadFollowupInput,
  accessContext?: LeadAccessContext
): Promise<LeadFollowupSummary> {
  const lead = await assertLeadBelongsToOrganization(leadId, organizationId, accessContext);

  await assertMasterOptionValue(
    organizationId,
    LEAD_OPTION_CATEGORIES.followupStatus,
    payload.followupStatus
  );

  const now = new Date();
  const followupId = crypto.randomUUID();
  const auditPayload: LeadFollowupAuditPayload = {
    id: followupId,
    leadId,
    followupDate: new Date(payload.followupDate).toISOString(),
    followupStatus: payload.followupStatus,
    note: payload.note?.trim() || null,
    nextFollowupDate: payload.nextFollowupDate
      ? new Date(payload.nextFollowupDate).toISOString()
      : null,
    createdBy: userId,
    createdAt: now.toISOString()
  };

  await db
    .update(crmLeads)
    .set({
      followupStatus: payload.followupStatus,
      updatedAt: now
    })
    .where(eq(crmLeads.id, lead.id));

  await auditAction({
    organizationId,
    branchId: lead.branchId,
    userId,
    entityType: 'crm_lead',
    entityId: leadId,
    action: 'create_lead_followup',
    afterData: auditPayload
  });

  const [userRow, followupStatuses] = await Promise.all([
    db.query.users.findFirst({
      where: eq(users.id, userId)
    }),
    getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId })
  ]);
  const labelMap = new Map(followupStatuses.map((item) => [item.id, item.label]));

  return {
    id: followupId,
    leadId,
    followupDate: auditPayload.followupDate,
    followupStatus: payload.followupStatus,
    followupStatusLabel: labelMap.get(payload.followupStatus) ?? null,
    note: auditPayload.note,
    nextFollowupDate: auditPayload.nextFollowupDate,
    createdAt: auditPayload.createdAt,
    createdBy: userId,
    createdByName: userRow?.name ?? null
  };
}

export function buildLeadAccessContext(input: {
  organizationId: string;
  userId: string;
  membership: {
    role: string;
    businessRole: string;
    businessRoles?: string[];
    permissions?: string[] | null;
    branchScopeIds?: string[] | null;
    productTypeScopeIds?: string[] | null;
    ownershipScope?: string | null;
    branchScopeMode?: string | null;
    productScopeMode?: string | null;
    approvalAuthority?: string | null;
  };
}) {
  return buildCrmSecurityContext(input);
}
