import { and, asc, count, desc, eq, ilike, inArray, isNull, or, sql, type SQL } from 'drizzle-orm';
import { crmCustomerContacts, crmCustomers, msOptions, users } from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
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 type {
  BranchOption,
  CustomerActivityRecord,
  CustomerContactMutationPayload,
  CustomerContactRecord,
  CustomerFilters,
  CustomerListItem,
  CustomerMutationPayload,
  CustomerOption,
  CustomerRecord,
  CustomerReferenceData
} from '../api/types';

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

let customerSubGroupColumnAvailable: boolean | undefined;

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 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,
    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
  };
}

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,
  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
): 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
  };
}

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 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);

  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)] : []),
    ...(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] =
    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))
    ]);

  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)
  };
}

export async function listCustomers(
  organizationId: string,
  filters: CustomerFilters
): Promise<{ items: CustomerListItem[]; totalItems: number }> {
  const includeCustomerSubGroup = await hasCustomerSubGroupColumn();
  const page = filters.page ?? 1;
  const limit = filters.limit ?? 10;
  const whereFilters = buildCustomerFilters(organizationId, filters);
  const where = whereFilters.length === 1 ? whereFilters[0] : and(...whereFilters);
  const offset = (page - 1) * limit;

  const [totalResult] = await db.select({ value: count() }).from(crmCustomers).where(where);
  const rows = await db
    .select(getCustomerRecordSelection(includeCustomerSubGroup))
    .from(crmCustomers)
    .where(where)
    .orderBy(parseSort(filters.sort))
    .limit(limit)
    .offset(offset);

  const customerIds = rows.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: rows.map((row) => ({
      ...mapCustomerRecord(row),
      contactCount: countMap.get(row.id) ?? 0
    })),
    totalItems: totalResult?.value ?? 0
  };
}

export async function getCustomerDetail(
  id: string,
  organizationId: string
): Promise<CustomerRecord> {
  const customer = await assertCustomerBelongsToOrganization(id, organizationId);
  return mapCustomerRecord(customer);
}

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 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;
}

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

  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) {
  await assertCustomerBelongsToOrganization(id, organizationId);

  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) {
  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)]
  });

  return rows.map(mapCustomerContactRecord);
}

export async function createCustomerContact(
  customerId: string,
  organizationId: string,
  userId: string,
  payload: CustomerContactMutationPayload
) {
  await assertCustomerBelongsToOrganization(customerId, organizationId);

  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
) {
  await assertCustomerBelongsToOrganization(customerId, organizationId);
  await assertContactBelongsToCustomer(contactId, customerId, organizationId);

  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
) {
  await assertCustomerBelongsToOrganization(customerId, organizationId);
  await assertContactBelongsToCustomer(contactId, customerId, organizationId);

  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 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;
}
