import 'server-only';

import { and, eq, isNull, sql } from 'drizzle-orm';
import {
  crmApprovalMatrices,
  crmApprovalSteps,
  crmApprovalWorkflows,
  crmContactShares,
  crmCustomerContacts,
  crmCustomers,
  crmLeads,
  crmOpportunities,
  crmOpportunityCustomers,
  crmQuotationCustomers,
  crmQuotationItems,
  crmQuotationTopics,
  crmQuotations,
  crmRoleProfiles,
  crmUserRoleAssignments,
  documentSequences,
  memberships,
  msOptions,
  organizations,
  users
} from '@/db/schema';
import { db } from '@/lib/db';
import { normalizeEmail, normalizeLower, normalizeOrganizationCode, pipeList } from './value-validator';
import type {
  CsvParsedFile,
  CsvParsedRow,
  CsvPreviewBatchIndex,
  CsvPreviewError,
  CsvPreviewWarning
} from './types';

type ExistingState = {
  exists: boolean;
};

type ResolverResult = {
  errors: CsvPreviewError[];
  warnings: CsvPreviewWarning[];
  exists: boolean;
};

type OrganizationReference = {
  id: string | null;
  key: string;
  exists: boolean;
};

function key(...parts: Array<string | number | null | undefined>): string {
  return parts.map((part) => String(part ?? '').trim().toLowerCase()).join('|');
}

function issue(
  file: string,
  row: number,
  column: string,
  code: string,
  message: string,
  suggestedFix: string
): CsvPreviewError {
  return { file, row, column, code, message, suggestedFix };
}

async function first<T>(promise: Promise<T[]>): Promise<T | null> {
  const [row] = await promise;
  return row ?? null;
}

async function existingUser(email: string): Promise<ExistingState> {
  if (!email) return { exists: false };
  const row = await first(
    db
      .select({ id: users.id })
      .from(users)
      .where(sql`lower(${users.email}) = ${normalizeEmail(email)}`)
      .limit(1)
  );
  return { exists: Boolean(row) };
}

async function resolveOrganization(
  organizationCode: string,
  batch: CsvPreviewBatchIndex
): Promise<OrganizationReference> {
  const normalizedCode = normalizeOrganizationCode(organizationCode);
  const slug = normalizedCode.toLowerCase();
  const existsInBatch = batch.organizations.has(normalizedCode);
  const row = await first(
    db
      .select({ id: organizations.id })
      .from(organizations)
      .where(eq(organizations.slug, slug))
      .limit(1)
  );

  return {
    id: row?.id ?? null,
    key: normalizedCode,
    exists: Boolean(row) || existsInBatch
  };
}

async function existingMembership(organizationId: string | null, userEmail: string): Promise<boolean> {
  if (!organizationId || !userEmail) return false;
  const user = await first(
    db
      .select({ id: users.id })
      .from(users)
      .where(sql`lower(${users.email}) = ${normalizeEmail(userEmail)}`)
      .limit(1)
  );
  if (!user) return false;
  const membership = await first(
    db
      .select({ id: memberships.id })
      .from(memberships)
      .where(and(eq(memberships.organizationId, organizationId), eq(memberships.userId, user.id)))
      .limit(1)
  );
  return Boolean(membership);
}

async function existingOption(
  organizationId: string | null,
  category: string,
  code: string
): Promise<ExistingState> {
  if (!organizationId || !category || !code) return { exists: false };
  const row = await first(
    db
      .select({ id: msOptions.id })
      .from(msOptions)
      .where(
        and(
          eq(msOptions.organizationId, organizationId),
          eq(msOptions.category, category),
          eq(msOptions.code, code),
          isNull(msOptions.deletedAt)
        )
      )
      .limit(1)
  );
  return { exists: Boolean(row) };
}

async function existingRoleProfile(
  organizationId: string | null,
  roleCode: string
): Promise<ExistingState> {
  if (!organizationId || !roleCode) return { exists: false };
  const row = await first(
    db
      .select({ id: crmRoleProfiles.id })
      .from(crmRoleProfiles)
      .where(
        and(
          eq(crmRoleProfiles.organizationId, organizationId),
          eq(crmRoleProfiles.code, roleCode),
          isNull(crmRoleProfiles.deletedAt)
        )
      )
      .limit(1)
  );
  return { exists: Boolean(row) };
}

async function existingApprovalWorkflow(
  organizationId: string | null,
  workflowCode: string
): Promise<ExistingState> {
  if (!organizationId || !workflowCode) return { exists: false };
  const row = await first(
    db
      .select({ id: crmApprovalWorkflows.id })
      .from(crmApprovalWorkflows)
      .where(
        and(
          eq(crmApprovalWorkflows.organizationId, organizationId),
          eq(crmApprovalWorkflows.code, workflowCode),
          isNull(crmApprovalWorkflows.deletedAt)
        )
      )
      .limit(1)
  );
  return { exists: Boolean(row) };
}

async function existingCustomer(organizationId: string | null, customerCode: string): Promise<ExistingState> {
  if (!organizationId || !customerCode) return { exists: false };
  const row = await first(
    db
      .select({ id: crmCustomers.id })
      .from(crmCustomers)
      .where(
        and(
          eq(crmCustomers.organizationId, organizationId),
          eq(crmCustomers.code, customerCode),
          isNull(crmCustomers.deletedAt)
        )
      )
      .limit(1)
  );
  return { exists: Boolean(row) };
}

async function existingContact(
  organizationId: string | null,
  customerCode: string,
  contactEmail: string
): Promise<ExistingState> {
  if (!organizationId || !customerCode || !contactEmail) return { exists: false };
  const customer = await first(
    db
      .select({ id: crmCustomers.id })
      .from(crmCustomers)
      .where(
        and(
          eq(crmCustomers.organizationId, organizationId),
          eq(crmCustomers.code, customerCode),
          isNull(crmCustomers.deletedAt)
        )
      )
      .limit(1)
  );
  if (!customer) return { exists: false };
  const row = await first(
    db
      .select({ id: crmCustomerContacts.id })
      .from(crmCustomerContacts)
      .where(
        and(
          eq(crmCustomerContacts.organizationId, organizationId),
          eq(crmCustomerContacts.customerId, customer.id),
          sql`lower(${crmCustomerContacts.email}) = ${normalizeEmail(contactEmail)}`,
          isNull(crmCustomerContacts.deletedAt)
        )
      )
      .limit(1)
  );
  return { exists: Boolean(row) };
}

async function existingCodeRecord(
  organizationId: string | null,
  table: 'lead' | 'opportunity' | 'quotation',
  codeValue: string
): Promise<ExistingState> {
  if (!organizationId || !codeValue) return { exists: false };

  if (table === 'lead') {
    const row = await first(
      db
        .select({ id: crmLeads.id })
        .from(crmLeads)
        .where(
          and(
            eq(crmLeads.organizationId, organizationId),
            eq(crmLeads.code, codeValue),
            isNull(crmLeads.deletedAt)
          )
        )
        .limit(1)
    );
    return { exists: Boolean(row) };
  }

  if (table === 'opportunity') {
    const row = await first(
      db
        .select({ id: crmOpportunities.id })
        .from(crmOpportunities)
        .where(
          and(
            eq(crmOpportunities.organizationId, organizationId),
            eq(crmOpportunities.code, codeValue),
            isNull(crmOpportunities.deletedAt)
          )
        )
        .limit(1)
    );
    return { exists: Boolean(row) };
  }

  const row = await first(
    db
      .select({ id: crmQuotations.id })
      .from(crmQuotations)
      .where(
        and(
          eq(crmQuotations.organizationId, organizationId),
          eq(crmQuotations.code, codeValue),
          isNull(crmQuotations.deletedAt)
        )
      )
      .limit(1)
  );
  return { exists: Boolean(row) };
}

function batchHasOption(
  batch: CsvPreviewBatchIndex,
  organizationCode: string,
  category: string,
  codeValue: string
): boolean {
  const normalizedCategory = normalizeLower(category);
  const normalizedCode = normalizeLower(codeValue);
  const organizationKey = normalizeOrganizationCode(organizationCode);
  if (normalizedCategory === 'crm_branch') return batch.branches.has(key(organizationKey, normalizedCode));
  if (normalizedCategory === 'crm_product_type') return batch.productTypes.has(key(organizationKey, normalizedCode));
  return batch.masterOptions.has(key(organizationKey, normalizedCategory, normalizedCode));
}

async function requireOrganization(
  file: CsvParsedFile,
  row: CsvParsedRow,
  batch: CsvPreviewBatchIndex,
  errors: CsvPreviewError[]
): Promise<OrganizationReference> {
  const organizationCode = row.values.organization_code;
  const organization = await resolveOrganization(organizationCode, batch);
  if (!organization.exists) {
    errors.push(
      issue(
        file.fileName,
        row.rowNumber,
        'organization_code',
        'UNRESOLVED_REFERENCE',
        'Organization was not found in uploaded files or database.',
        'Upload organizations.csv first or use an existing organization code/slug.'
      )
    );
  }
  return organization;
}

async function requireUser(
  file: CsvParsedFile,
  row: CsvParsedRow,
  column: string,
  batch: CsvPreviewBatchIndex,
  errors: CsvPreviewError[]
): Promise<void> {
  const email = row.values[column];
  if (!email) return;
  if (batch.users.has(normalizeEmail(email)) || (await existingUser(email)).exists) return;
  errors.push(
    issue(
      file.fileName,
      row.rowNumber,
      column,
      'UNRESOLVED_REFERENCE',
      'User email was not found in uploaded files or database.',
      'Upload users.csv first or use an existing user email.'
    )
  );
}

async function requireOption(input: {
  file: CsvParsedFile;
  row: CsvParsedRow;
  column: string;
  category: string;
  organization: OrganizationReference;
  batch: CsvPreviewBatchIndex;
  errors: CsvPreviewError[];
}): Promise<void> {
  const codeValue = input.row.values[input.column];
  if (!codeValue) return;
  const normalized = normalizeLower(codeValue);
  const inBatch = batchHasOption(input.batch, input.organization.key, input.category, normalized);
  const inDb = await existingOption(input.organization.id, input.category, normalized);
  if (inBatch || inDb.exists) return;
  input.errors.push(
    issue(
      input.file.fileName,
      input.row.rowNumber,
      input.column,
      'UNKNOWN_OPTION',
      `${input.category} option "${codeValue}" was not found.`,
      'Upload the related master option file first or use an existing option code.'
    )
  );
}

async function requireWorkflow(
  file: CsvParsedFile,
  row: CsvParsedRow,
  organization: OrganizationReference,
  batch: CsvPreviewBatchIndex,
  errors: CsvPreviewError[]
): Promise<void> {
  const workflowCode = row.values.workflow_code;
  if (!workflowCode) return;
  if (
    batch.approvalWorkflows.has(key(organization.key, workflowCode)) ||
    (await existingApprovalWorkflow(organization.id, workflowCode)).exists
  ) {
    return;
  }
  errors.push(
    issue(
      file.fileName,
      row.rowNumber,
      'workflow_code',
      'UNRESOLVED_REFERENCE',
      'Approval workflow was not found.',
      'Upload approval-workflows.csv before approval steps or matrix rows.'
    )
  );
}

async function requireCustomer(
  file: CsvParsedFile,
  row: CsvParsedRow,
  organization: OrganizationReference,
  batch: CsvPreviewBatchIndex,
  errors: CsvPreviewError[]
): Promise<void> {
  const customerCode = row.values.customer_code;
  if (!customerCode) return;
  if (
    batch.customers.has(key(organization.key, customerCode)) ||
    (await existingCustomer(organization.id, customerCode)).exists
  ) {
    return;
  }
  errors.push(
    issue(
      file.fileName,
      row.rowNumber,
      'customer_code',
      'UNRESOLVED_REFERENCE',
      'Customer was not found.',
      'Upload customers.csv first or use an existing customer code.'
    )
  );
}

async function requireContact(
  file: CsvParsedFile,
  row: CsvParsedRow,
  organization: OrganizationReference,
  batch: CsvPreviewBatchIndex,
  errors: CsvPreviewError[]
): Promise<void> {
  const customerCode = row.values.customer_code;
  const contactEmail = row.values.contact_email;
  if (!customerCode || !contactEmail) return;
  if (
    batch.contactsByEmail.has(key(organization.key, customerCode, contactEmail)) ||
    (await existingContact(organization.id, customerCode, contactEmail)).exists
  ) {
    return;
  }
  errors.push(
    issue(
      file.fileName,
      row.rowNumber,
      'contact_email',
      'UNRESOLVED_REFERENCE',
      'Contact email was not found for the resolved customer.',
      'Upload contacts.csv first and ensure the contact belongs to the customer.'
    )
  );
}

async function resolveExistingForTemplate(
  file: CsvParsedFile,
  row: CsvParsedRow,
  organization: OrganizationReference
): Promise<boolean> {
  const values = row.values;

  switch (file.template.file) {
    case 'users.csv':
      return (await existingUser(values.email)).exists;
    case 'organizations.csv':
      return (await resolveOrganization(values.organization_code, emptyBatchIndex())).id !== null;
    case 'memberships.csv':
      return existingMembership(organization.id, values.user_email);
    case 'master-options.csv':
      return (await existingOption(organization.id, values.category, values.code)).exists;
    case 'branches.csv':
      return (await existingOption(organization.id, 'crm_branch', values.branch_code)).exists;
    case 'product-types.csv':
      return (await existingOption(organization.id, 'crm_product_type', values.product_type_code)).exists;
    case 'approval-workflows.csv':
      return (await existingApprovalWorkflow(organization.id, values.workflow_code)).exists;
    case 'customers.csv':
      return (await existingCustomer(organization.id, values.customer_code)).exists;
    case 'leads.csv':
      return (await existingCodeRecord(organization.id, 'lead', values.lead_code)).exists;
    case 'opportunities.csv':
      return (await existingCodeRecord(organization.id, 'opportunity', values.opportunity_code)).exists;
    case 'quotations.csv':
      return (await existingCodeRecord(organization.id, 'quotation', values.quotation_code)).exists;
    case 'document-sequences.csv':
      return existingDocumentSequence(organization.id, values);
    case 'crm-role-assignments.csv':
      return existingRoleAssignment(organization.id, values);
    case 'approval-steps.csv':
      return existingApprovalStep(organization.id, values);
    case 'approval-matrix.csv':
      return existingApprovalMatrix(organization.id, values);
    case 'contacts.csv':
      return existingContactByRow(organization.id, values);
    case 'contact-shares.csv':
      return existingContactShare(organization.id, values);
    case 'opportunity-parties.csv':
      return existingOpportunityParty(organization.id, values);
    case 'quotation-items.csv':
      return existingQuotationItem(organization.id, values);
    case 'quotation-parties.csv':
      return existingQuotationParty(organization.id, values);
    case 'quotation-topics.csv':
      return existingQuotationTopic(organization.id, values);
    case 'quotation-topic-items.csv':
      return existingQuotationTopicItem(organization.id, values);
    default:
      return false;
  }
}

async function existingDocumentSequence(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  if (!organizationId) return false;
  const branchId = values.branch_code ? normalizeLower(values.branch_code) : '';
  const productType = values.product_type_code ? normalizeLower(values.product_type_code) : 'all';
  const row = await first(
    db
      .select({ id: documentSequences.id })
      .from(documentSequences)
      .where(
        and(
          eq(documentSequences.organizationId, organizationId),
          eq(documentSequences.branchId, branchId),
          eq(documentSequences.productType, productType),
          eq(documentSequences.documentType, values.document_type),
          eq(documentSequences.period, values.period)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingRoleAssignment(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  if (!organizationId) return false;
  const user = await first(
    db.select({ id: users.id }).from(users).where(sql`lower(${users.email}) = ${normalizeEmail(values.user_email)}`).limit(1)
  );
  const role = await first(
    db
      .select({ id: crmRoleProfiles.id })
      .from(crmRoleProfiles)
      .where(and(eq(crmRoleProfiles.organizationId, organizationId), eq(crmRoleProfiles.code, values.crm_role_code)))
      .limit(1)
  );
  if (!user || !role) return false;
  const row = await first(
    db
      .select({ id: crmUserRoleAssignments.id })
      .from(crmUserRoleAssignments)
      .where(
        and(
          eq(crmUserRoleAssignments.organizationId, organizationId),
          eq(crmUserRoleAssignments.userId, user.id),
          eq(crmUserRoleAssignments.roleProfileId, role.id),
          isNull(crmUserRoleAssignments.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function workflowIdByCode(organizationId: string | null, workflowCode: string): Promise<string | null> {
  if (!organizationId || !workflowCode) return null;
  const workflow = await first(
    db
      .select({ id: crmApprovalWorkflows.id })
      .from(crmApprovalWorkflows)
      .where(and(eq(crmApprovalWorkflows.organizationId, organizationId), eq(crmApprovalWorkflows.code, workflowCode)))
      .limit(1)
  );
  return workflow?.id ?? null;
}

async function quotationIdByCode(organizationId: string | null, quotationCode: string): Promise<string | null> {
  if (!organizationId || !quotationCode) return null;
  const quotation = await first(
    db
      .select({ id: crmQuotations.id })
      .from(crmQuotations)
      .where(and(eq(crmQuotations.organizationId, organizationId), eq(crmQuotations.code, quotationCode)))
      .limit(1)
  );
  return quotation?.id ?? null;
}

async function opportunityIdByCode(organizationId: string | null, opportunityCode: string): Promise<string | null> {
  if (!organizationId || !opportunityCode) return null;
  const opportunity = await first(
    db
      .select({ id: crmOpportunities.id })
      .from(crmOpportunities)
      .where(and(eq(crmOpportunities.organizationId, organizationId), eq(crmOpportunities.code, opportunityCode)))
      .limit(1)
  );
  return opportunity?.id ?? null;
}

async function customerIdByCode(organizationId: string | null, customerCode: string): Promise<string | null> {
  if (!organizationId || !customerCode) return null;
  const customer = await first(
    db
      .select({ id: crmCustomers.id })
      .from(crmCustomers)
      .where(and(eq(crmCustomers.organizationId, organizationId), eq(crmCustomers.code, customerCode)))
      .limit(1)
  );
  return customer?.id ?? null;
}

async function existingApprovalStep(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  const workflowId = await workflowIdByCode(organizationId, values.workflow_code);
  if (!workflowId || !values.step_number) return false;
  const row = await first(
    db
      .select({ id: crmApprovalSteps.id })
      .from(crmApprovalSteps)
      .where(
        and(
          eq(crmApprovalSteps.workflowId, workflowId),
          eq(crmApprovalSteps.stepNumber, Number(values.step_number)),
          isNull(crmApprovalSteps.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingApprovalMatrix(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  const workflowId = await workflowIdByCode(organizationId, values.workflow_code);
  if (!organizationId || !workflowId) return false;
  const row = await first(
    db
      .select({ id: crmApprovalMatrices.id })
      .from(crmApprovalMatrices)
      .where(
        and(
          eq(crmApprovalMatrices.organizationId, organizationId),
          eq(crmApprovalMatrices.workflowId, workflowId),
          eq(crmApprovalMatrices.entityType, values.entity_type),
          eq(crmApprovalMatrices.priority, Number(values.priority)),
          isNull(crmApprovalMatrices.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingContactByRow(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  if (values.email) return (await existingContact(organizationId, values.customer_code, values.email)).exists;
  const customerId = await customerIdByCode(organizationId, values.customer_code);
  if (!organizationId || !customerId) return false;
  const row = await first(
    db
      .select({ id: crmCustomerContacts.id })
      .from(crmCustomerContacts)
      .where(
        and(
          eq(crmCustomerContacts.organizationId, organizationId),
          eq(crmCustomerContacts.customerId, customerId),
          eq(crmCustomerContacts.name, values.contact_name),
          isNull(crmCustomerContacts.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingContactShare(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  const customerId = await customerIdByCode(organizationId, values.customer_code);
  if (!organizationId || !customerId || !values.contact_email || !values.shared_to_user_email) return false;
  const contact = await first(
    db
      .select({ id: crmCustomerContacts.id })
      .from(crmCustomerContacts)
      .where(
        and(
          eq(crmCustomerContacts.organizationId, organizationId),
          eq(crmCustomerContacts.customerId, customerId),
          sql`lower(${crmCustomerContacts.email}) = ${normalizeEmail(values.contact_email)}`,
          isNull(crmCustomerContacts.deletedAt)
        )
      )
      .limit(1)
  );
  const sharedTo = await first(
    db
      .select({ id: users.id })
      .from(users)
      .where(sql`lower(${users.email}) = ${normalizeEmail(values.shared_to_user_email)}`)
      .limit(1)
  );
  if (!contact || !sharedTo) return false;
  const row = await first(
    db
      .select({ id: crmContactShares.id })
      .from(crmContactShares)
      .where(
        and(
          eq(crmContactShares.organizationId, organizationId),
          eq(crmContactShares.contactId, contact.id),
          eq(crmContactShares.sharedToUserId, sharedTo.id),
          isNull(crmContactShares.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingOpportunityParty(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  const opportunityId = await opportunityIdByCode(organizationId, values.opportunity_code);
  const customerId = await customerIdByCode(organizationId, values.customer_code);
  if (!opportunityId || !customerId) return false;
  const row = await first(
    db
      .select({ id: crmOpportunityCustomers.id })
      .from(crmOpportunityCustomers)
      .where(
        and(
          eq(crmOpportunityCustomers.opportunityId, opportunityId),
          eq(crmOpportunityCustomers.customerId, customerId),
          eq(crmOpportunityCustomers.role, values.role_code),
          isNull(crmOpportunityCustomers.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingQuotationItem(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  const quotationId = await quotationIdByCode(organizationId, values.quotation_code);
  if (!quotationId || !values.item_number) return false;
  const row = await first(
    db
      .select({ id: crmQuotationItems.id })
      .from(crmQuotationItems)
      .where(
        and(
          eq(crmQuotationItems.quotationId, quotationId),
          eq(crmQuotationItems.itemNumber, Number(values.item_number)),
          isNull(crmQuotationItems.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingQuotationParty(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  const quotationId = await quotationIdByCode(organizationId, values.quotation_code);
  const customerId = await customerIdByCode(organizationId, values.customer_code);
  if (!quotationId || !customerId) return false;
  const row = await first(
    db
      .select({ id: crmQuotationCustomers.id })
      .from(crmQuotationCustomers)
      .where(
        and(
          eq(crmQuotationCustomers.quotationId, quotationId),
          eq(crmQuotationCustomers.customerId, customerId),
          eq(crmQuotationCustomers.role, values.role_code),
          isNull(crmQuotationCustomers.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingQuotationTopic(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  const quotationId = await quotationIdByCode(organizationId, values.quotation_code);
  if (!organizationId || !quotationId || !values.topic_type || !values.title) return false;
  const row = await first(
    db
      .select({ id: crmQuotationTopics.id })
      .from(crmQuotationTopics)
      .where(
        and(
          eq(crmQuotationTopics.organizationId, organizationId),
          eq(crmQuotationTopics.quotationId, quotationId),
          eq(crmQuotationTopics.topicType, values.topic_type),
          eq(crmQuotationTopics.title, values.title),
          isNull(crmQuotationTopics.deletedAt)
        )
      )
      .limit(1)
  );
  return Boolean(row);
}

async function existingQuotationTopicItem(
  organizationId: string | null,
  values: Record<string, string>
): Promise<boolean> {
  void organizationId;
  void values;
  return false;
}

function emptyBatchIndex(): CsvPreviewBatchIndex {
  return {
    organizations: new Set(),
    users: new Set(),
    memberships: new Set(),
    roleAssignments: new Set(),
    masterOptions: new Set(),
    branches: new Set(),
    productTypes: new Set(),
    documentSequences: new Set(),
    approvalWorkflows: new Set(),
    approvalSteps: new Set(),
    approvalMatrices: new Set(),
    customers: new Set(),
    contactsByEmail: new Set(),
    contactsByName: new Set(),
    contactShares: new Set(),
    leads: new Set(),
    opportunities: new Set(),
    opportunityParties: new Set(),
    quotations: new Set(),
    quotationItems: new Set(),
    quotationParties: new Set(),
    quotationTopics: new Set(),
    quotationTopicItems: new Set()
  };
}

export function buildCsvPreviewBatchIndex(files: CsvParsedFile[]): CsvPreviewBatchIndex {
  const index = emptyBatchIndex();

  files.forEach((file) => {
    file.rows.forEach((row) => {
      const values = row.values;
      const organizationCode = values.organization_code ? normalizeOrganizationCode(values.organization_code) : '';

      switch (file.template.file) {
        case 'organizations.csv':
          if (values.organization_code) index.organizations.add(normalizeOrganizationCode(values.organization_code));
          break;
        case 'users.csv':
          index.users.add(normalizeEmail(values.email));
          break;
        case 'memberships.csv':
          index.memberships.add(key(organizationCode, values.user_email));
          break;
        case 'crm-role-assignments.csv':
          index.roleAssignments.add(key(organizationCode, values.user_email, values.crm_role_code));
          break;
        case 'master-options.csv':
          index.masterOptions.add(key(organizationCode, values.category, values.code));
          break;
        case 'branches.csv':
          index.branches.add(key(organizationCode, values.branch_code));
          break;
        case 'product-types.csv':
          index.productTypes.add(key(organizationCode, values.product_type_code));
          break;
        case 'document-sequences.csv':
          index.documentSequences.add(
            key(
              organizationCode,
              values.branch_code,
              values.product_type_code || 'all',
              values.document_type,
              values.period
            )
          );
          break;
        case 'approval-workflows.csv':
          index.approvalWorkflows.add(key(organizationCode, values.workflow_code));
          break;
        case 'approval-steps.csv':
          index.approvalSteps.add(key(organizationCode, values.workflow_code, values.step_number));
          break;
        case 'approval-matrix.csv':
          index.approvalMatrices.add(key(organizationCode, values.entity_type, values.workflow_code, values.priority));
          break;
        case 'customers.csv':
          index.customers.add(key(organizationCode, values.customer_code));
          break;
        case 'contacts.csv':
          if (values.email) index.contactsByEmail.add(key(organizationCode, values.customer_code, values.email));
          index.contactsByName.add(key(organizationCode, values.customer_code, values.contact_name));
          break;
        case 'contact-shares.csv':
          index.contactShares.add(key(organizationCode, values.customer_code, values.contact_email, values.shared_to_user_email));
          break;
        case 'leads.csv':
          index.leads.add(key(organizationCode, values.lead_code));
          break;
        case 'opportunities.csv':
          index.opportunities.add(key(organizationCode, values.opportunity_code));
          break;
        case 'opportunity-parties.csv':
          index.opportunityParties.add(key(organizationCode, values.opportunity_code, values.customer_code, values.role_code));
          break;
        case 'quotations.csv':
          index.quotations.add(key(organizationCode, values.quotation_code));
          break;
        case 'quotation-items.csv':
          index.quotationItems.add(key(organizationCode, values.quotation_code, values.item_number));
          break;
        case 'quotation-parties.csv':
          index.quotationParties.add(key(organizationCode, values.quotation_code, values.customer_code, values.role_code));
          break;
        case 'quotation-topics.csv':
          index.quotationTopics.add(key(organizationCode, values.quotation_code, values.topic_key));
          break;
        case 'quotation-topic-items.csv':
          index.quotationTopicItems.add(
            key(organizationCode, values.quotation_code, values.topic_key, values.sort_order, values.content)
          );
          break;
        default:
          break;
      }
    });
  });

  return index;
}

async function validateCommonReferences(
  file: CsvParsedFile,
  row: CsvParsedRow,
  batch: CsvPreviewBatchIndex,
  errors: CsvPreviewError[]
): Promise<OrganizationReference> {
  const organization = row.values.organization_code
    ? await requireOrganization(file, row, batch, errors)
    : { id: null, key: '', exists: false };

  const userColumns = [
    'created_by_email',
    'updated_by_email',
    'user_email',
    'owner_user_email',
    'owner_marketing_user_email',
    'assigned_sales_owner_email',
    'assigned_to_user_email',
    'salesman_email',
    'shared_to_user_email',
    'shared_by_user_email'
  ];
  for (const column of userColumns) {
    await requireUser(file, row, column, batch, errors);
  }

  if (row.values.branch_code) {
    await requireOption({ file, row, column: 'branch_code', category: 'crm_branch', organization, batch, errors });
  }
  if (row.values.product_type_code) {
    await requireOption({
      file,
      row,
      column: 'product_type_code',
      category: 'crm_product_type',
      organization,
      batch,
      errors
    });
  }

  return organization;
}

export async function resolveCsvRowDependencies(
  file: CsvParsedFile,
  row: CsvParsedRow,
  batch: CsvPreviewBatchIndex
): Promise<ResolverResult> {
  const errors: CsvPreviewError[] = [];
  const warnings: CsvPreviewWarning[] = [];
  const organization = await validateCommonReferences(file, row, batch, errors);

  switch (file.template.file) {
    case 'crm-role-assignments.csv':
      if (
        row.values.crm_role_code &&
        !(await existingRoleProfile(organization.id, row.values.crm_role_code)).exists
      ) {
        errors.push(
          issue(
            file.fileName,
            row.rowNumber,
            'crm_role_code',
            'UNRESOLVED_REFERENCE',
            'CRM role profile was not found.',
            'Run foundation role setup before assigning CRM roles.'
          )
        );
      }
      for (const branchCode of pipeList(row.values.branch_codes ?? '')) {
        await requireOption({
          file,
          row: { ...row, values: { ...row.values, branch_code: branchCode } },
          column: 'branch_code',
          category: 'crm_branch',
          organization,
          batch,
          errors
        });
      }
      for (const productTypeCode of pipeList(row.values.product_type_codes ?? '')) {
        await requireOption({
          file,
          row: { ...row, values: { ...row.values, product_type_code: productTypeCode } },
          column: 'product_type_code',
          category: 'crm_product_type',
          organization,
          batch,
          errors
        });
      }
      break;
    case 'master-options.csv':
      if (row.values.parent_category && row.values.parent_code) {
        await requireOption({
          file,
          row: { ...row, values: { ...row.values, parent_code: row.values.parent_code } },
          column: 'parent_code',
          category: row.values.parent_category,
          organization,
          batch,
          errors
        });
      }
      break;
    case 'approval-steps.csv':
    case 'approval-matrix.csv':
      await requireWorkflow(file, row, organization, batch, errors);
      if (
        file.template.file === 'approval-steps.csv' &&
        row.values.role_code &&
        !(await existingRoleProfile(organization.id, row.values.role_code)).exists
      ) {
        warnings.push(
          issue(
            file.fileName,
            row.rowNumber,
            'role_code',
            'UNRESOLVED_ROLE_PROFILE',
            'Approval step role was not found in CRM role profiles.',
            'Confirm the role is an approved workflow role or seed the CRM role profile first.'
          )
        );
      }
      if (row.values.currency) {
        await requireOption({ file, row, column: 'currency', category: 'crm_currency', organization, batch, errors });
      }
      break;
    case 'customers.csv':
      for (const [column, category] of [
        ['customer_type', 'crm_customer_type'],
        ['customer_status', 'crm_customer_status'],
        ['lead_channel', 'crm_lead_channel'],
        ['customer_group', 'crm_customer_group'],
        ['customer_sub_group', 'crm_customer_sub_group']
      ] as const) {
        await requireOption({ file, row, column, category, organization, batch, errors });
      }
      break;
    case 'contacts.csv':
    case 'contact-shares.csv':
      await requireCustomer(file, row, organization, batch, errors);
      if (file.template.file === 'contact-shares.csv') {
        await requireContact(file, row, organization, batch, errors);
        if (row.values.shared_to_user_email === row.values.shared_by_user_email) {
          warnings.push(
            issue(
              file.fileName,
              row.rowNumber,
              'shared_to_user_email',
              'SELF_SHARE',
              'Contact is shared to the same user who shared it.',
              'Confirm whether this share row is needed.'
            )
          );
        }
      }
      break;
    case 'leads.csv':
      await requireCustomer(file, row, organization, batch, errors);
      await requireContact(file, row, organization, batch, errors);
      for (const [column, category] of [
        ['status', 'crm_lead_status'],
        ['lead_channel', 'crm_lead_channel'],
        ['priority', 'crm_priority'],
        ['awareness_code', 'crm_lead_awareness'],
        ['followup_status', 'crm_lead_followup_status'],
        ['lost_reason', 'crm_lead_lost_reason']
      ] as const) {
        await requireOption({ file, row, column, category, organization, batch, errors });
      }
      break;
    case 'opportunities.csv':
      await requireCustomer(file, row, organization, batch, errors);
      await requireContact(file, row, organization, batch, errors);
      if (
        row.values.lead_code &&
        !batch.leads.has(key(organization.key, row.values.lead_code)) &&
        !(await existingCodeRecord(organization.id, 'lead', row.values.lead_code)).exists
      ) {
        errors.push(issue(file.fileName, row.rowNumber, 'lead_code', 'UNRESOLVED_REFERENCE', 'Lead was not found.', 'Upload leads.csv first or use an existing lead code.'));
      }
      for (const [column, category] of [
        ['status', 'crm_opportunity_status'],
        ['priority', 'crm_priority'],
        ['lead_channel', 'crm_lead_channel']
      ] as const) {
        await requireOption({ file, row, column, category, organization, batch, errors });
      }
      break;
    case 'opportunity-parties.csv':
      await requireCustomer(file, row, organization, batch, errors);
      if (
        !batch.opportunities.has(key(organization.key, row.values.opportunity_code)) &&
        !(await existingCodeRecord(organization.id, 'opportunity', row.values.opportunity_code)).exists
      ) {
        errors.push(issue(file.fileName, row.rowNumber, 'opportunity_code', 'UNRESOLVED_REFERENCE', 'Opportunity was not found.', 'Upload opportunities.csv first or use an existing opportunity code.'));
      }
      await requireOption({ file, row, column: 'role_code', category: 'crm_project_party_role', organization, batch, errors });
      break;
    case 'quotations.csv':
      await requireCustomer(file, row, organization, batch, errors);
      await requireContact(file, row, organization, batch, errors);
      if (
        row.values.opportunity_code &&
        !batch.opportunities.has(key(organization.key, row.values.opportunity_code)) &&
        !(await existingCodeRecord(organization.id, 'opportunity', row.values.opportunity_code)).exists
      ) {
        errors.push(issue(file.fileName, row.rowNumber, 'opportunity_code', 'UNRESOLVED_REFERENCE', 'Opportunity was not found.', 'Upload opportunities.csv first or use an existing opportunity code.'));
      }
      for (const [column, category] of [
        ['quotation_type', 'crm_quotation_type'],
        ['status', 'crm_quotation_status'],
        ['currency', 'crm_currency'],
        ['discount_type', 'crm_discount_type'],
        ['sent_via', 'crm_sent_via']
      ] as const) {
        await requireOption({ file, row, column, category, organization, batch, errors });
      }
      break;
    case 'quotation-items.csv':
      if (
        !batch.quotations.has(key(organization.key, row.values.quotation_code)) &&
        !(await existingCodeRecord(organization.id, 'quotation', row.values.quotation_code)).exists
      ) {
        errors.push(issue(file.fileName, row.rowNumber, 'quotation_code', 'UNRESOLVED_REFERENCE', 'Quotation was not found.', 'Upload quotations.csv first or use an existing quotation code.'));
      }
      await requireOption({ file, row, column: 'unit', category: 'crm_unit', organization, batch, errors });
      await requireOption({ file, row, column: 'discount_type', category: 'crm_discount_type', organization, batch, errors });
      break;
    case 'quotation-parties.csv':
      await requireCustomer(file, row, organization, batch, errors);
      if (
        !batch.quotations.has(key(organization.key, row.values.quotation_code)) &&
        !(await existingCodeRecord(organization.id, 'quotation', row.values.quotation_code)).exists
      ) {
        errors.push(issue(file.fileName, row.rowNumber, 'quotation_code', 'UNRESOLVED_REFERENCE', 'Quotation was not found.', 'Upload quotations.csv first or use an existing quotation code.'));
      }
      await requireOption({ file, row, column: 'role_code', category: 'crm_project_party_role', organization, batch, errors });
      break;
    case 'quotation-topics.csv':
      if (
        !batch.quotations.has(key(organization.key, row.values.quotation_code)) &&
        !(await existingCodeRecord(organization.id, 'quotation', row.values.quotation_code)).exists
      ) {
        errors.push(issue(file.fileName, row.rowNumber, 'quotation_code', 'UNRESOLVED_REFERENCE', 'Quotation was not found.', 'Upload quotations.csv first or use an existing quotation code.'));
      }
      await requireOption({ file, row, column: 'topic_type', category: 'crm_quotation_topic_type', organization, batch, errors });
      break;
    case 'quotation-topic-items.csv':
      if (!batch.quotationTopics.has(key(organization.key, row.values.quotation_code, row.values.topic_key))) {
        const existingTopic = await existingQuotationTopic(organization.id, row.values);
        if (!existingTopic) {
          errors.push(issue(file.fileName, row.rowNumber, 'topic_key', 'UNRESOLVED_REFERENCE', 'Quotation topic was not found.', 'Upload quotation-topics.csv first or use an existing topic key.'));
        }
      }
      break;
    default:
      break;
  }

  return {
    errors,
    warnings,
    exists: await resolveExistingForTemplate(file, row, organization)
  };
}
