import {
  and,
  asc,
  count,
  desc,
  eq,
  ilike,
  inArray,
  isNull,
  or,
  sql,
  type SQL,
} from "drizzle-orm";
import {
  crmContactShares,
  crmCustomerContacts,
  crmCustomerOwnerHistory,
  crmCustomers,
  crmOpportunities,
  crmQuotations,
  memberships,
  msOptions,
  users,
} from "@/db/schema";
import { db } from "@/lib/db";
import { AuthError } from "@/lib/auth/session";
import {
  hasBranchScopeAccess,
  hasProductScopeAccess,
} from "@/lib/auth/crm-access";
import { listAuditLogs } from "@/features/foundation/audit-log/service";
import {
  validateBranchAccess,
  getUserBranches,
} from "@/features/foundation/branch-scope/service";
import { generateNextDocumentCode } from "@/features/foundation/document-sequence/service";
import { getActiveOptionsByCategory } from "@/features/foundation/master-options/service";
import {
  canAccessContact,
  canAccessCustomer,
  type CrmSecurityContext,
} from "@/features/crm/security/server/service";
import type {
  BranchOption,
  CustomerActivityRecord,
  CustomerContactMutationPayload,
  CustomerContactRecord,
  CustomerFilters,
  CustomerListItem,
  CustomerMutationPayload,
  CustomerOption,
  CustomerOwnerHistoryRecord,
  CustomerOwnerMutationPayload,
  CustomerRecord,
  CustomerReferenceData,
} from "../api/types";

type CustomerRecordSource = Omit<
  typeof crmCustomers.$inferSelect,
  "customerSubGroup"
> & {
  customerSubGroup?: string | null;
  ownerName?: string | null;
  ownerAssignedByName?: string | null;
};

let customerSubGroupColumnAvailable: boolean | undefined;

export type CustomerAccessContext = CrmSecurityContext;

const CUSTOMER_OPTION_CATEGORIES = {
  customerType: "crm_customer_type",
  customerStatus: "crm_customer_status",
  leadChannel: "crm_lead_channel",
  customerGroup: "crm_customer_group",
  customerSubGroup: "crm_customer_sub_group",
} as const;

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

function mapCustomerOptionRow(
  row: typeof msOptions.$inferSelect,
): CustomerOption {
  return {
    id: row.id,
    code: row.code,
    label: row.label,
    value: row.value,
    parentId: row.parentId,
  };
}

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

function mapCustomerUserLookup(row: {
  userId: string;
  name: string;
  membershipRole: string;
  businessRole: string;
}) {
  return {
    id: row.userId,
    name: row.name,
    membershipRole: row.membershipRole === "admin" ? "admin" : "user",
    businessRole: row.businessRole,
  } as const;
}

function mapOwnerHistoryRecord(
  row: typeof crmCustomerOwnerHistory.$inferSelect & {
    oldOwnerName?: string | null;
    newOwnerName?: string | null;
    changedByName?: string | null;
  },
): CustomerOwnerHistoryRecord {
  return {
    id: row.id,
    customerId: row.customerId,
    oldOwnerUserId: row.oldOwnerUserId ?? null,
    oldOwnerName: row.oldOwnerName ?? null,
    newOwnerUserId: row.newOwnerUserId ?? null,
    newOwnerName: row.newOwnerName ?? null,
    changedBy: row.changedBy,
    changedByName: row.changedByName ?? null,
    changedAt: row.changedAt.toISOString(),
    remark: row.remark ?? null,
  };
}

function toIsoDateTime(value: Date | string) {
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}

function toOptionalIsoDateTime(value: Date | string | null | undefined) {
  return value ? toIsoDateTime(value) : null;
}

function mapCustomerRecord(row: CustomerRecordSource): CustomerRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    branchId: row.branchId,
    code: row.code,
    name: row.name,
    abbr: row.abbr,
    taxId: row.taxId,
    customerType: row.customerType,
    customerStatus: row.customerStatus,
    address: row.address,
    province: row.province,
    district: row.district,
    subDistrict: row.subDistrict,
    postalCode: row.postalCode,
    country: row.country,
    phone: row.phone,
    fax: row.fax,
    email: row.email,
    website: row.website,
    leadChannel: row.leadChannel,
    customerGroup: row.customerGroup,
    customerSubGroup: row.customerSubGroup ?? null,
    ownerUserId: row.ownerUserId ?? null,
    ownerAssignedAt: toOptionalIsoDateTime(row.ownerAssignedAt),
    ownerAssignedBy: row.ownerAssignedBy ?? null,
    ownerName: row.ownerName ?? null,
    ownerAssignedByName: row.ownerAssignedByName ?? null,
    notes: row.notes,
    isActive: row.isActive,
    createdAt: toIsoDateTime(row.createdAt),
    updatedAt: toIsoDateTime(row.updatedAt),
    deletedAt: toOptionalIsoDateTime(row.deletedAt),
    createdBy: row.createdBy,
    updatedBy: row.updatedBy,
  };
}

const baseCustomerRecordSelection = {
  id: crmCustomers.id,
  organizationId: crmCustomers.organizationId,
  branchId: crmCustomers.branchId,
  code: crmCustomers.code,
  name: crmCustomers.name,
  abbr: crmCustomers.abbr,
  taxId: crmCustomers.taxId,
  customerType: crmCustomers.customerType,
  customerStatus: crmCustomers.customerStatus,
  address: crmCustomers.address,
  province: crmCustomers.province,
  district: crmCustomers.district,
  subDistrict: crmCustomers.subDistrict,
  postalCode: crmCustomers.postalCode,
  country: crmCustomers.country,
  phone: crmCustomers.phone,
  fax: crmCustomers.fax,
  email: crmCustomers.email,
  website: crmCustomers.website,
  leadChannel: crmCustomers.leadChannel,
  customerGroup: crmCustomers.customerGroup,
  notes: crmCustomers.notes,
  ownerUserId: crmCustomers.ownerUserId,
  ownerAssignedAt: crmCustomers.ownerAssignedAt,
  ownerAssignedBy: crmCustomers.ownerAssignedBy,
  isActive: crmCustomers.isActive,
  createdAt: crmCustomers.createdAt,
  updatedAt: crmCustomers.updatedAt,
  deletedAt: crmCustomers.deletedAt,
  createdBy: crmCustomers.createdBy,
  updatedBy: crmCustomers.updatedBy,
} as const;

function getCustomerRecordSelection(includeCustomerSubGroup: boolean) {
  return includeCustomerSubGroup
    ? {
        ...baseCustomerRecordSelection,
        customerSubGroup: crmCustomers.customerSubGroup,
      }
    : baseCustomerRecordSelection;
}

async function hasCustomerSubGroupColumn() {
  if (customerSubGroupColumnAvailable !== undefined) {
    return customerSubGroupColumnAvailable;
  }

  const result = await db.execute<{ exists: boolean }>(sql`
    select exists (
      select 1
      from information_schema.columns
      where table_schema = 'public'
        and table_name = 'crm_customers'
        and column_name = 'customer_sub_group'
    ) as "exists"
  `);

  customerSubGroupColumnAvailable = Boolean(result[0]?.exists);

  return customerSubGroupColumnAvailable;
}

function mapCustomerContactRecord(
  row: typeof crmCustomerContacts.$inferSelect,
  shares: CustomerContactRecord["shares"] = [],
): CustomerContactRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    customerId: row.customerId,
    name: row.name,
    position: row.position,
    department: row.department,
    phone: row.phone,
    mobile: row.mobile,
    email: row.email,
    isPrimary: row.isPrimary,
    notes: row.notes,
    isActive: row.isActive,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null,
    createdBy: row.createdBy,
    updatedBy: row.updatedBy,
    shares,
  };
}

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

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

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

    const sortMap = {
      code: crmCustomers.code,
      name: crmCustomers.name,
      customerStatus: crmCustomers.customerStatus,
      customerType: crmCustomers.customerType,
      branch: crmCustomers.branchId,
      updatedAt: crmCustomers.updatedAt,
    } as const;

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

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

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

function splitFilterValue(value?: string) {
  return value
    ? value
        .split(/[.,]/)
        .map((item) => item.trim())
        .filter(Boolean)
    : [];
}

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

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 includeCustomerSubGroup = await hasCustomerSubGroupColumn();
  const [customer] = await db
    .select(getCustomerRecordSelection(includeCustomerSubGroup))
    .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 listCrmUsersForOrganization(organizationId: string) {
  const rows = await db
    .select({
      userId: memberships.userId,
      name: users.name,
      membershipRole: memberships.role,
      businessRole: memberships.businessRole,
    })
    .from(memberships)
    .innerJoin(users, eq(memberships.userId, users.id))
    .where(eq(memberships.organizationId, organizationId))
    .orderBy(asc(users.name));

  return [...new Map(rows.map((row) => [row.userId, row])).values()].map(
    mapCustomerUserLookup,
  );
}

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

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

async function listContactSharesMap(
  contactIds: string[],
  organizationId: string,
) {
  if (!contactIds.length) {
    return new Map<string, CustomerContactRecord["shares"]>();
  }

  const shareRows = await db
    .select({
      id: crmContactShares.id,
      contactId: crmContactShares.contactId,
      sharedToUserId: crmContactShares.sharedToUserId,
      sharedToName: users.name,
      sharedByUserId: crmContactShares.sharedByUserId,
      sharedAt: crmContactShares.sharedAt,
      isActive: crmContactShares.isActive,
      remark: crmContactShares.remark,
    })
    .from(crmContactShares)
    .leftJoin(users, eq(users.id, crmContactShares.sharedToUserId))
    .where(
      and(
        eq(crmContactShares.organizationId, organizationId),
        inArray(crmContactShares.contactId, contactIds),
        eq(crmContactShares.isActive, true),
        isNull(crmContactShares.deletedAt),
      ),
    )
    .orderBy(desc(crmContactShares.sharedAt));

  const sharedByIds = [...new Set(shareRows.map((row) => row.sharedByUserId))];
  const sharedByRows = sharedByIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, sharedByIds))
    : [];
  const sharedByMap = new Map(sharedByRows.map((row) => [row.id, row.name]));
  const shareMap = new Map<string, CustomerContactRecord["shares"]>();

  for (const row of shareRows) {
    const items = shareMap.get(row.contactId) ?? [];
    items.push({
      id: row.id,
      contactId: row.contactId,
      sharedToUserId: row.sharedToUserId,
      sharedToName: row.sharedToName ?? null,
      sharedByUserId: row.sharedByUserId,
      sharedByName: sharedByMap.get(row.sharedByUserId) ?? null,
      sharedAt: row.sharedAt.toISOString(),
      isActive: row.isActive,
      remark: row.remark ?? null,
    });
    shareMap.set(row.contactId, items);
  }

  return shareMap;
}

async function resolveAccessibleCustomerRelationSets(
  organizationId: string,
  accessContext: CustomerAccessContext,
) {
  const [opportunities, quotations] = await Promise.all([
    db
      .select({
        customerId: crmOpportunities.customerId,
        createdBy: crmOpportunities.createdBy,
        assignedToUserId: crmOpportunities.assignedToUserId,
        branchId: crmOpportunities.branchId,
        productType: crmOpportunities.productType,
      })
      .from(crmOpportunities)
      .where(
        and(
          eq(crmOpportunities.organizationId, organizationId),
          isNull(crmOpportunities.deletedAt),
        ),
      ),
    db
      .select({
        customerId: crmQuotations.customerId,
        createdBy: crmQuotations.createdBy,
        salesmanId: crmQuotations.salesmanId,
        branchId: crmQuotations.branchId,
        quotationType: crmQuotations.quotationType,
      })
      .from(crmQuotations)
      .where(
        and(
          eq(crmQuotations.organizationId, organizationId),
          isNull(crmQuotations.deletedAt),
        ),
      ),
  ]);

  const opportunityCustomerIds = new Set<string>();
  const quotationCustomerIds = new Set<string>();

  for (const row of opportunities) {
    if (!hasBranchScopeAccess(accessContext, row.branchId)) {
      continue;
    }

    if (!hasProductScopeAccess(accessContext, row.productType)) {
      continue;
    }

    if (
      accessContext.membershipRole === "admin" ||
      accessContext.ownershipScope === "organization" ||
      accessContext.ownershipScope === "team" ||
      accessContext.ownershipScope === "monitor" ||
      row.createdBy === accessContext.userId ||
      row.assignedToUserId === accessContext.userId
    ) {
      opportunityCustomerIds.add(row.customerId);
    }
  }

  for (const row of quotations) {
    if (!hasBranchScopeAccess(accessContext, row.branchId)) {
      continue;
    }

    if (!hasProductScopeAccess(accessContext, row.quotationType)) {
      continue;
    }

    if (
      accessContext.membershipRole === "admin" ||
      accessContext.ownershipScope === "organization" ||
      accessContext.ownershipScope === "team" ||
      row.createdBy === accessContext.userId ||
      row.salesmanId === accessContext.userId
    ) {
      quotationCustomerIds.add(row.customerId);
    }
  }

  return { opportunityCustomerIds, quotationCustomerIds };
}

async function canAccessCustomerRow(
  row: CustomerRecordSource,
  accessContext: CustomerAccessContext,
  relationSets?: Awaited<
    ReturnType<typeof resolveAccessibleCustomerRelationSets>
  >,
) {
  if (!canAccessCustomer(accessContext, row)) {
    if (
      accessContext.membershipRole === "admin" ||
      accessContext.ownershipScope === "organization" ||
      accessContext.ownershipScope === "team"
    ) {
      return false;
    }
  }

  if (!hasBranchScopeAccess(accessContext, row.branchId)) {
    return false;
  }

  if (
    accessContext.membershipRole === "admin" ||
    accessContext.ownershipScope === "organization" ||
    accessContext.ownershipScope === "team"
  ) {
    return true;
  }

  const { opportunityCustomerIds, quotationCustomerIds } =
    relationSets ??
    (await resolveAccessibleCustomerRelationSets(
      row.organizationId,
      accessContext,
    ));

  if (accessContext.ownershipScope === "monitor") {
    return opportunityCustomerIds.has(row.id);
  }

  return (
    row.createdBy === accessContext.userId ||
    row.ownerUserId === accessContext.userId ||
    opportunityCustomerIds.has(row.id) ||
    quotationCustomerIds.has(row.id)
  );
}

async function assertCustomerAccess(
  id: string,
  organizationId: string,
  accessContext?: CustomerAccessContext,
) {
  const customer = await assertCustomerBelongsToOrganization(
    id,
    organizationId,
  );

  if (accessContext && !(await canAccessCustomerRow(customer, accessContext))) {
    throw new AuthError("Forbidden", 403);
  }

  return customer;
}

async function canAccessContactRow(
  contact: typeof crmCustomerContacts.$inferSelect,
  customer: CustomerRecordSource,
  accessContext: CustomerAccessContext,
  shares?: CustomerContactRecord["shares"],
) {
  return canAccessContact(accessContext, {
    branchId: customer.branchId,
    createdBy: contact.createdBy,
    ownerUserId: customer.ownerUserId,
    sharedToUserIds: (shares ?? []).map((share) => share.sharedToUserId),
  });
}

export async function assertContactAccess(
  customerId: string,
  contactId: string,
  organizationId: string,
  accessContext?: CustomerAccessContext,
) {
  const customer = await assertCustomerBelongsToOrganization(
    customerId,
    organizationId,
  );
  const contact = await assertContactBelongsToCustomer(
    contactId,
    customerId,
    organizationId,
  );

  if (accessContext) {
    const shares =
      (await listContactSharesMap([contact.id], organizationId)).get(
        contact.id,
      ) ?? [];
    const visible = await canAccessContactRow(
      contact,
      customer,
      accessContext,
      shares,
    );

    if (!visible) {
      throw new AuthError("Forbidden", 403);
    }
  }

  return { customer, contact };
}

async function assertContactBelongsToCustomer(
  contactId: string,
  customerId: string,
  organizationId: string,
) {
  const contact = await db.query.crmCustomerContacts.findFirst({
    where: and(
      eq(crmCustomerContacts.id, contactId),
      eq(crmCustomerContacts.customerId, customerId),
      eq(crmCustomerContacts.organizationId, organizationId),
      isNull(crmCustomerContacts.deletedAt),
    ),
  });

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

  return contact;
}

async function validateCustomerPayload(
  organizationId: string,
  payload: CustomerMutationPayload,
) {
  await assertMasterOptionValue(
    organizationId,
    CUSTOMER_OPTION_CATEGORIES.customerType,
    payload.customerType,
  );
  await assertMasterOptionValue(
    organizationId,
    CUSTOMER_OPTION_CATEGORIES.customerStatus,
    payload.customerStatus,
  );
  await assertMasterOptionValue(
    organizationId,
    CUSTOMER_OPTION_CATEGORIES.leadChannel,
    payload.leadChannel ?? null,
  );
  await assertMasterOptionValue(
    organizationId,
    CUSTOMER_OPTION_CATEGORIES.customerGroup,
    payload.customerGroup ?? null,
  );
  await assertMasterOptionValue(
    organizationId,
    CUSTOMER_OPTION_CATEGORIES.customerSubGroup,
    payload.customerSubGroup ?? null,
  );

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

  if (payload.customerSubGroup) {
    if (!payload.customerGroup) {
      throw new AuthError("Customer sub group requires a customer group", 400);
    }

    const subGroup = await db.query.msOptions.findFirst({
      where: and(
        eq(msOptions.organizationId, organizationId),
        eq(msOptions.category, CUSTOMER_OPTION_CATEGORIES.customerSubGroup),
        eq(msOptions.id, payload.customerSubGroup),
        isNull(msOptions.deletedAt),
      ),
    });

    if (!subGroup) {
      throw new AuthError("Invalid customer sub group", 400);
    }

    if (subGroup.parentId !== payload.customerGroup) {
      throw new AuthError(
        "Selected customer sub group does not belong to the selected customer group",
        400,
      );
    }
  }
}

function buildCustomerFilters(
  organizationId: string,
  filters: CustomerFilters,
): SQL[] {
  const statuses = splitFilterValue(filters.customerStatus);
  const customerTypes = splitFilterValue(filters.customerType);
  const branches = splitFilterValue(filters.branch);
  const owners = splitFilterValue(filters.ownerUserId);

  return [
    eq(crmCustomers.organizationId, organizationId),
    isNull(crmCustomers.deletedAt),
    ...(statuses.length
      ? [inArray(crmCustomers.customerStatus, statuses)]
      : []),
    ...(customerTypes.length
      ? [inArray(crmCustomers.customerType, customerTypes)]
      : []),
    ...(branches.length ? [inArray(crmCustomers.branchId, branches)] : []),
    ...(owners.length ? [inArray(crmCustomers.ownerUserId, owners)] : []),
    ...(filters.search
      ? [
          or(
            ilike(crmCustomers.code, `%${filters.search}%`),
            ilike(crmCustomers.name, `%${filters.search}%`),
            ilike(crmCustomers.abbr, `%${filters.search}%`),
            ilike(crmCustomers.taxId, `%${filters.search}%`),
            ilike(crmCustomers.email, `%${filters.search}%`),
          )!,
        ]
      : []),
  ];
}

export async function getCustomerReferenceData(
  organizationId: string,
): Promise<CustomerReferenceData> {
  const [
    branches,
    customerTypes,
    customerStatuses,
    leadChannels,
    customerGroups,
    customerSubGroups,
    crmUsers,
  ] = await Promise.all([
    getUserBranches(),
    getActiveOptionsByCategory(CUSTOMER_OPTION_CATEGORIES.customerType, {
      organizationId,
    }),
    getActiveOptionsByCategory(CUSTOMER_OPTION_CATEGORIES.customerStatus, {
      organizationId,
    }),
    getActiveOptionsByCategory(CUSTOMER_OPTION_CATEGORIES.leadChannel, {
      organizationId,
    }),
    getActiveOptionsByCategory(CUSTOMER_OPTION_CATEGORIES.customerGroup, {
      organizationId,
    }),
    db
      .select()
      .from(msOptions)
      .where(
        and(
          eq(msOptions.organizationId, organizationId),
          eq(msOptions.category, CUSTOMER_OPTION_CATEGORIES.customerSubGroup),
          eq(msOptions.isActive, true),
          isNull(msOptions.deletedAt),
        ),
      )
      .orderBy(asc(msOptions.sortOrder), asc(msOptions.label)),
    listCrmUsersForOrganization(organizationId),
  ]);

  return {
    branches: branches.map(mapBranchOption),
    customerTypes: customerTypes.map(mapCustomerOption),
    customerStatuses: customerStatuses.map(mapCustomerOption),
    leadChannels: leadChannels.map(mapCustomerOption),
    customerGroups: customerGroups.map(mapCustomerOption),
    customerSubGroups: customerSubGroups.map(mapCustomerOptionRow),
    crmUsers,
  };
}

export async function listCustomers(
  organizationId: string,
  filters: CustomerFilters,
  accessContext?: CustomerAccessContext,
): Promise<{ items: CustomerListItem[]; totalItems: number }> {
  const page = filters.page ?? 1;
  const limit = filters.limit ?? 10;
  const whereFilters = buildCustomerFilters(organizationId, filters);
  if (filters.myCustomers === "true" && accessContext) {
    whereFilters.push(eq(crmCustomers.ownerUserId, accessContext.userId));
  }
  const where =
    whereFilters.length === 1 ? whereFilters[0] : and(...whereFilters);
  const offset = (page - 1) * limit;

  const rows = await db.execute<CustomerRecordSource>(sql`
    select
      crm_customers.id,
      crm_customers.organization_id as "organizationId",
      crm_customers.branch_id as "branchId",
      crm_customers.code,
      crm_customers.name,
      crm_customers.abbr,
      crm_customers.tax_id as "taxId",
      crm_customers.customer_type as "customerType",
      crm_customers.customer_status as "customerStatus",
      crm_customers.address,
      crm_customers.province,
      crm_customers.district,
      crm_customers.sub_district as "subDistrict",
      crm_customers.postal_code as "postalCode",
      crm_customers.country,
      crm_customers.phone,
      crm_customers.fax,
      crm_customers.email,
      crm_customers.website,
      crm_customers.lead_channel as "leadChannel",
      crm_customers.customer_group as "customerGroup",
      crm_customers.customer_sub_group as "customerSubGroup",
      crm_customers.owner_user_id as "ownerUserId",
      crm_customers.owner_assigned_at as "ownerAssignedAt",
      crm_customers.owner_assigned_by as "ownerAssignedBy",
      crm_customers.notes,
      crm_customers.is_active as "isActive",
      crm_customers.created_at as "createdAt",
      crm_customers.updated_at as "updatedAt",
      crm_customers.deleted_at as "deletedAt",
      crm_customers.created_by as "createdBy",
      crm_customers.updated_by as "updatedBy",
      owner_u.name as "ownerName",
      assigned_by_u.name as "ownerAssignedByName"
    from crm_customers
    left join users owner_u on owner_u.id = crm_customers.owner_user_id
    left join users assigned_by_u on assigned_by_u.id = crm_customers.owner_assigned_by
    where ${where}
    order by ${parseSort(filters.sort)}
  `);
  const relationSets = accessContext
    ? await resolveAccessibleCustomerRelationSets(organizationId, accessContext)
    : null;

  const scopedRows = accessContext
    ? (
        await Promise.all(
          rows.map(async (row) =>
            (await canAccessCustomerRow(
              row,
              accessContext,
              relationSets ?? undefined,
            ))
              ? row
              : null,
          ),
        )
      ).filter((row): row is CustomerRecordSource => row !== null)
    : rows;
  const pagedRows = scopedRows.slice(offset, offset + limit);

  const customerIds = pagedRows.map((row) => row.id);
  const contactCounts = customerIds.length
    ? await db
        .select({
          customerId: crmCustomerContacts.customerId,
          value: count(),
        })
        .from(crmCustomerContacts)
        .where(
          and(
            eq(crmCustomerContacts.organizationId, organizationId),
            inArray(crmCustomerContacts.customerId, customerIds),
            isNull(crmCustomerContacts.deletedAt),
          ),
        )
        .groupBy(crmCustomerContacts.customerId)
    : [];
  const countMap = new Map(
    contactCounts.map((item) => [item.customerId, item.value]),
  );

  return {
    items: pagedRows.map((row) => ({
      ...mapCustomerRecord(row),
      contactCount: countMap.get(row.id) ?? 0,
    })),
    totalItems: scopedRows.length,
  };
}

export async function getCustomerDetail(
  id: string,
  organizationId: string,
  accessContext?: CustomerAccessContext,
): Promise<CustomerRecord> {
  const customer = await assertCustomerAccess(
    id,
    organizationId,
    accessContext,
  );
  const relatedUserIds = [
    customer.ownerUserId,
    customer.ownerAssignedBy,
  ].filter(Boolean) as string[];
  const userRows = relatedUserIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, relatedUserIds))
    : [];
  const userMap = new Map(userRows.map((row) => [row.id, row.name]));

  return mapCustomerRecord({
    ...customer,
    ownerName: customer.ownerUserId
      ? (userMap.get(customer.ownerUserId) ?? null)
      : null,
    ownerAssignedByName: customer.ownerAssignedBy
      ? (userMap.get(customer.ownerAssignedBy) ?? null)
      : null,
  });
}

export async function getCustomerActivity(
  id: string,
  organizationId: string,
): Promise<CustomerActivityRecord[]> {
  const auditLogs = await listAuditLogs({
    organizationId,
    entityType: "crm_customer",
    entityId: id,
    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,
  }));
}

export async function getCustomerOwnerHistory(
  id: string,
  organizationId: string,
  accessContext?: CustomerAccessContext,
): Promise<CustomerOwnerHistoryRecord[]> {
  await assertCustomerAccess(id, organizationId, accessContext);
  const rows = await db.query.crmCustomerOwnerHistory.findMany({
    where: and(
      eq(crmCustomerOwnerHistory.customerId, id),
      eq(crmCustomerOwnerHistory.organizationId, organizationId),
      isNull(crmCustomerOwnerHistory.deletedAt),
    ),
    orderBy: [desc(crmCustomerOwnerHistory.changedAt)],
  });
  const userIds = [
    ...new Set(
      rows
        .flatMap((row) => [
          row.oldOwnerUserId,
          row.newOwnerUserId,
          row.changedBy,
        ])
        .filter(Boolean) as string[],
    ),
  ];
  const userRows = userIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, userIds))
    : [];
  const userMap = new Map(userRows.map((row) => [row.id, row.name]));

  return rows.map((row) =>
    mapOwnerHistoryRecord({
      ...row,
      oldOwnerName: row.oldOwnerUserId
        ? (userMap.get(row.oldOwnerUserId) ?? null)
        : null,
      newOwnerName: row.newOwnerUserId
        ? (userMap.get(row.newOwnerUserId) ?? null)
        : null,
      changedByName: userMap.get(row.changedBy) ?? null,
    }),
  );
}

export async function createCustomer(
  organizationId: string,
  userId: string,
  payload: CustomerMutationPayload,
) {
  await validateCustomerPayload(organizationId, payload);

  const documentCode = await generateNextDocumentCode({
    organizationId,
    documentType: "customer",
    branchId: payload.branchId ?? "",
  });

  const [created] = await db
    .insert(crmCustomers)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      branchId: payload.branchId ?? null,
      code: documentCode.code,
      name: payload.name.trim(),
      abbr: payload.abbr?.trim() || null,
      taxId: payload.taxId?.trim() || null,
      customerType: payload.customerType,
      customerStatus: payload.customerStatus,
      address: payload.address?.trim() || null,
      province: payload.province?.trim() || null,
      district: payload.district?.trim() || null,
      subDistrict: payload.subDistrict?.trim() || null,
      postalCode: payload.postalCode?.trim() || null,
      country: payload.country?.trim() || null,
      phone: payload.phone?.trim() || null,
      fax: payload.fax?.trim() || null,
      email: payload.email?.trim() || null,
      website: payload.website?.trim() || null,
      leadChannel: payload.leadChannel ?? null,
      customerGroup: payload.customerGroup ?? null,
      customerSubGroup: payload.customerSubGroup ?? null,
      notes: payload.notes?.trim() || null,
      isActive: payload.isActive ?? true,
      createdBy: userId,
      updatedBy: userId,
    })
    .returning();

  return created;
}

async function recordCustomerOwnerHistory(input: {
  organizationId: string;
  customerId: string;
  oldOwnerUserId?: string | null;
  newOwnerUserId?: string | null;
  changedBy: string;
  remark?: string | null;
}) {
  await db.insert(crmCustomerOwnerHistory).values({
    id: crypto.randomUUID(),
    organizationId: input.organizationId,
    customerId: input.customerId,
    oldOwnerUserId: input.oldOwnerUserId ?? null,
    newOwnerUserId: input.newOwnerUserId ?? null,
    changedBy: input.changedBy,
    changedAt: new Date(),
    remark: input.remark?.trim() || null,
  });
}

export async function assignCustomerOwner(
  customerId: string,
  organizationId: string,
  userId: string,
  payload: CustomerOwnerMutationPayload,
  accessContext?: CustomerAccessContext,
) {
  if (!payload.ownerUserId) {
    throw new AuthError("Owner user is required", 400);
  }

  const current = await assertCustomerAccess(
    customerId,
    organizationId,
    accessContext,
  );
  await assertOwnerUserBelongsToOrganization(
    payload.ownerUserId,
    organizationId,
  );

  if (current.ownerUserId === payload.ownerUserId) {
    return current;
  }

  const [updated] = await db
    .update(crmCustomers)
    .set({
      ownerUserId: payload.ownerUserId,
      ownerAssignedAt: new Date(),
      ownerAssignedBy: userId,
      updatedAt: new Date(),
      updatedBy: userId,
    })
    .where(eq(crmCustomers.id, customerId))
    .returning();

  await recordCustomerOwnerHistory({
    organizationId,
    customerId,
    oldOwnerUserId: current.ownerUserId,
    newOwnerUserId: payload.ownerUserId,
    changedBy: userId,
    remark: payload.remark,
  });

  return updated;
}

export async function clearCustomerOwner(
  customerId: string,
  organizationId: string,
  userId: string,
  payload?: Pick<CustomerOwnerMutationPayload, "remark">,
  accessContext?: CustomerAccessContext,
) {
  const current = await assertCustomerAccess(
    customerId,
    organizationId,
    accessContext,
  );

  if (!current.ownerUserId) {
    return current;
  }

  const [updated] = await db
    .update(crmCustomers)
    .set({
      ownerUserId: null,
      ownerAssignedAt: null,
      ownerAssignedBy: null,
      updatedAt: new Date(),
      updatedBy: userId,
    })
    .where(eq(crmCustomers.id, customerId))
    .returning();

  await recordCustomerOwnerHistory({
    organizationId,
    customerId,
    oldOwnerUserId: current.ownerUserId,
    newOwnerUserId: null,
    changedBy: userId,
    remark: payload?.remark,
  });

  return updated;
}

export async function updateCustomer(
  id: string,
  organizationId: string,
  userId: string,
  payload: CustomerMutationPayload,
  accessContext?: CustomerAccessContext,
) {
  await validateCustomerPayload(organizationId, payload);
  await assertCustomerAccess(id, organizationId, accessContext);

  const [updated] = await db
    .update(crmCustomers)
    .set({
      branchId: payload.branchId ?? null,
      name: payload.name.trim(),
      abbr: payload.abbr?.trim() || null,
      taxId: payload.taxId?.trim() || null,
      customerType: payload.customerType,
      customerStatus: payload.customerStatus,
      address: payload.address?.trim() || null,
      province: payload.province?.trim() || null,
      district: payload.district?.trim() || null,
      subDistrict: payload.subDistrict?.trim() || null,
      postalCode: payload.postalCode?.trim() || null,
      country: payload.country?.trim() || null,
      phone: payload.phone?.trim() || null,
      fax: payload.fax?.trim() || null,
      email: payload.email?.trim() || null,
      website: payload.website?.trim() || null,
      leadChannel: payload.leadChannel ?? null,
      customerGroup: payload.customerGroup ?? null,
      customerSubGroup: payload.customerSubGroup ?? null,
      notes: payload.notes?.trim() || null,
      isActive: payload.isActive ?? true,
      updatedBy: userId,
      updatedAt: new Date(),
    })
    .where(eq(crmCustomers.id, id))
    .returning();

  return updated;
}

export async function softDeleteCustomer(
  id: string,
  organizationId: string,
  userId: string,
  accessContext?: CustomerAccessContext,
) {
  await assertCustomerAccess(id, organizationId, accessContext);

  const now = new Date();

  const [updated] = await db
    .update(crmCustomers)
    .set({
      deletedAt: now,
      updatedAt: now,
      updatedBy: userId,
      isActive: false,
    })
    .where(eq(crmCustomers.id, id))
    .returning();

  await db
    .update(crmCustomerContacts)
    .set({
      deletedAt: now,
      updatedAt: now,
      updatedBy: userId,
      isActive: false,
    })
    .where(
      and(
        eq(crmCustomerContacts.customerId, id),
        eq(crmCustomerContacts.organizationId, organizationId),
      ),
    );

  return updated;
}

export async function listCustomerContacts(
  id: string,
  organizationId: string,
  accessContext?: CustomerAccessContext,
) {
  const customer = await assertCustomerBelongsToOrganization(
    id,
    organizationId,
  );
  const rows = await db.query.crmCustomerContacts.findMany({
    where: and(
      eq(crmCustomerContacts.customerId, id),
      eq(crmCustomerContacts.organizationId, organizationId),
      isNull(crmCustomerContacts.deletedAt),
    ),
    orderBy: [
      desc(crmCustomerContacts.isPrimary),
      asc(crmCustomerContacts.name),
    ],
  });
  const shareMap = await listContactSharesMap(
    rows.map((row) => row.id),
    organizationId,
  );
  const scopedRows = accessContext
    ? (
        await Promise.all(
          rows.map(async (row) =>
            (await canAccessContactRow(
              row,
              customer,
              accessContext,
              shareMap.get(row.id) ?? [],
            ))
              ? row
              : null,
          ),
        )
      ).filter(
        (row): row is typeof crmCustomerContacts.$inferSelect => row !== null,
      )
    : rows;

  if (accessContext && !scopedRows.length) {
    const canViewCustomer = await canAccessCustomerRow(customer, accessContext);

    if (!canViewCustomer) {
      throw new AuthError("Forbidden", 403);
    }
  }

  return scopedRows.map((row) =>
    mapCustomerContactRecord(row, shareMap.get(row.id) ?? []),
  );
}

export async function createCustomerContact(
  customerId: string,
  organizationId: string,
  userId: string,
  payload: CustomerContactMutationPayload,
  accessContext?: CustomerAccessContext,
) {
  await assertCustomerAccess(customerId, organizationId, accessContext);

  return db.transaction(async (tx) => {
    if (payload.isPrimary) {
      await tx
        .update(crmCustomerContacts)
        .set({
          isPrimary: false,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where(
          and(
            eq(crmCustomerContacts.customerId, customerId),
            eq(crmCustomerContacts.organizationId, organizationId),
            isNull(crmCustomerContacts.deletedAt),
          ),
        );
    }

    const [created] = await tx
      .insert(crmCustomerContacts)
      .values({
        id: crypto.randomUUID(),
        organizationId,
        customerId,
        name: payload.name.trim(),
        position: payload.position?.trim() || null,
        department: payload.department?.trim() || null,
        phone: payload.phone?.trim() || null,
        mobile: payload.mobile?.trim() || null,
        email: payload.email?.trim() || null,
        isPrimary: payload.isPrimary ?? false,
        notes: payload.notes?.trim() || null,
        isActive: payload.isActive ?? true,
        createdBy: userId,
        updatedBy: userId,
      })
      .returning();

    return created;
  });
}

export async function updateCustomerContact(
  customerId: string,
  contactId: string,
  organizationId: string,
  userId: string,
  payload: CustomerContactMutationPayload,
  accessContext?: CustomerAccessContext,
) {
  await assertContactAccess(
    customerId,
    contactId,
    organizationId,
    accessContext,
  );

  return db.transaction(async (tx) => {
    if (payload.isPrimary) {
      await tx
        .update(crmCustomerContacts)
        .set({
          isPrimary: false,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where(
          and(
            eq(crmCustomerContacts.customerId, customerId),
            eq(crmCustomerContacts.organizationId, organizationId),
            isNull(crmCustomerContacts.deletedAt),
          ),
        );
    }

    const [updated] = await tx
      .update(crmCustomerContacts)
      .set({
        name: payload.name.trim(),
        position: payload.position?.trim() || null,
        department: payload.department?.trim() || null,
        phone: payload.phone?.trim() || null,
        mobile: payload.mobile?.trim() || null,
        email: payload.email?.trim() || null,
        isPrimary: payload.isPrimary ?? false,
        notes: payload.notes?.trim() || null,
        isActive: payload.isActive ?? true,
        updatedAt: new Date(),
        updatedBy: userId,
      })
      .where(eq(crmCustomerContacts.id, contactId))
      .returning();

    return updated;
  });
}

export async function softDeleteCustomerContact(
  customerId: string,
  contactId: string,
  organizationId: string,
  userId: string,
  accessContext?: CustomerAccessContext,
) {
  await assertContactAccess(
    customerId,
    contactId,
    organizationId,
    accessContext,
  );

  const [updated] = await db
    .update(crmCustomerContacts)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date(),
      updatedBy: userId,
      isActive: false,
      isPrimary: false,
    })
    .where(eq(crmCustomerContacts.id, contactId))
    .returning();

  return updated;
}

export async function shareCustomerContact(
  customerId: string,
  contactId: string,
  organizationId: string,
  userId: string,
  payload: { sharedToUserId: string; remark?: string | null },
  accessContext?: CustomerAccessContext,
) {
  await assertCustomerAccess(customerId, organizationId, accessContext);
  await assertContactBelongsToCustomer(contactId, customerId, organizationId);
  await assertOwnerUserBelongsToOrganization(
    payload.sharedToUserId,
    organizationId,
  );

  const existing = await db.query.crmContactShares.findFirst({
    where: and(
      eq(crmContactShares.organizationId, organizationId),
      eq(crmContactShares.contactId, contactId),
      eq(crmContactShares.sharedToUserId, payload.sharedToUserId),
    ),
  });

  if (existing) {
    const [updated] = await db
      .update(crmContactShares)
      .set({
        sharedByUserId: userId,
        sharedAt: new Date(),
        isActive: true,
        remark: payload.remark?.trim() || null,
        deletedAt: null,
        updatedAt: new Date(),
      })
      .where(eq(crmContactShares.id, existing.id))
      .returning();

    return updated;
  }

  const [created] = await db
    .insert(crmContactShares)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      contactId,
      sharedToUserId: payload.sharedToUserId,
      sharedByUserId: userId,
      sharedAt: new Date(),
      isActive: true,
      remark: payload.remark?.trim() || null,
    })
    .returning();

  return created;
}

export async function listCustomerContactShares(
  customerId: string,
  contactId: string,
  organizationId: string,
  accessContext?: CustomerAccessContext,
) {
  await assertContactAccess(
    customerId,
    contactId,
    organizationId,
    accessContext,
  );
  return (
    (await listContactSharesMap([contactId], organizationId)).get(contactId) ??
    []
  );
}

export async function unshareCustomerContact(
  customerId: string,
  contactId: string,
  shareId: string,
  organizationId: string,
  accessContext?: CustomerAccessContext,
) {
  await assertCustomerAccess(customerId, organizationId, accessContext);
  await assertContactBelongsToCustomer(contactId, customerId, organizationId);

  const [updated] = await db
    .update(crmContactShares)
    .set({
      isActive: false,
      deletedAt: new Date(),
      updatedAt: new Date(),
    })
    .where(
      and(
        eq(crmContactShares.id, shareId),
        eq(crmContactShares.organizationId, organizationId),
        eq(crmContactShares.contactId, contactId),
      ),
    )
    .returning();

  if (!updated) {
    throw new AuthError("Share not found", 404);
  }

  return updated;
}

export async function resolveCustomerOptionLabel(
  organizationId: string,
  category: string,
  id: string | null,
) {
  if (!id) {
    return null;
  }

  const row = await db.query.msOptions.findFirst({
    where: and(
      eq(msOptions.organizationId, organizationId),
      eq(msOptions.category, category),
      eq(msOptions.id, id),
      isNull(msOptions.deletedAt),
    ),
  });

  return row?.label ?? null;
}

