import { and, asc, eq, isNull } from 'drizzle-orm';
import {
  crmDocumentTemplateMappings,
  crmDocumentTemplateTableColumns,
  crmDocumentTemplateVersions,
  crmDocumentTemplates
} from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import {
  formatPdfCurrency,
  formatPdfDate,
  normalizePdfmeTable
} from '@/features/crm/quotations/document/server/pdfme-transforms';
import type {
  DocumentTemplateDetail,
  DocumentTemplateFilters,
  DocumentTemplateListItem,
  DocumentTemplateMappingMutationPayload,
  DocumentTemplateMappingRecord,
  DocumentTemplateMappingWithColumns,
  DocumentTemplateMutationPayload,
  DocumentTemplateRecord,
  DocumentTemplateTableColumnRecord,
  DocumentTemplateVersionDetail,
  DocumentTemplateVersionMutationPayload,
  DocumentTemplateVersionRecord,
  ResolveTemplateParams,
  ResolvedDocumentTemplate
} from '../types';

function mapTemplateRecord(row: typeof crmDocumentTemplates.$inferSelect): DocumentTemplateRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    documentType: row.documentType,
    productType: row.productType,
    fileType: row.fileType,
    templateName: row.templateName,
    description: row.description,
    isDefault: row.isDefault,
    isActive: row.isActive,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null,
    createdBy: row.createdBy,
    updatedBy: row.updatedBy
  };
}

function mapVersionRecord(
  row: typeof crmDocumentTemplateVersions.$inferSelect
): DocumentTemplateVersionRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    templateId: row.templateId,
    version: row.version,
    filePath: row.filePath,
    schemaJson: row.schemaJson,
    previewImageUrl: row.previewImageUrl,
    isActive: row.isActive,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null,
    createdBy: row.createdBy
  };
}

function mapMappingRecord(
  row: typeof crmDocumentTemplateMappings.$inferSelect
): DocumentTemplateMappingRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    templateVersionId: row.templateVersionId,
    placeholderKey: row.placeholderKey,
    sourcePath: row.sourcePath,
    dataType: row.dataType as DocumentTemplateMappingRecord['dataType'],
    sheetName: row.sheetName,
    defaultValue: row.defaultValue,
    formatMask: row.formatMask,
    sortOrder: row.sortOrder,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

function mapTableColumnRecord(
  row: typeof crmDocumentTemplateTableColumns.$inferSelect
): DocumentTemplateTableColumnRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    mappingId: row.mappingId,
    columnName: row.columnName,
    sourceField: row.sourceField,
    columnLetter: row.columnLetter,
    sortOrder: row.sortOrder,
    formatMask: row.formatMask,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

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

  if (!template) {
    throw new AuthError('Document template not found', 404);
  }

  return template;
}

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

  if (!version) {
    throw new AuthError('Document template version not found', 404);
  }

  return version;
}

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

  if (!mapping) {
    throw new AuthError('Document template mapping not found', 404);
  }

  return mapping;
}

export async function resolveTemplateMappings(
  templateVersionId: string,
  organizationId: string
): Promise<DocumentTemplateMappingWithColumns[]> {
  const mappings = await db
    .select()
    .from(crmDocumentTemplateMappings)
    .where(
      and(
        eq(crmDocumentTemplateMappings.templateVersionId, templateVersionId),
        eq(crmDocumentTemplateMappings.organizationId, organizationId),
        isNull(crmDocumentTemplateMappings.deletedAt)
      )
    )
    .orderBy(asc(crmDocumentTemplateMappings.sortOrder), asc(crmDocumentTemplateMappings.createdAt));
  const columns = await db
    .select()
    .from(crmDocumentTemplateTableColumns)
    .where(
      and(
        eq(crmDocumentTemplateTableColumns.organizationId, organizationId),
        isNull(crmDocumentTemplateTableColumns.deletedAt)
      )
    )
    .orderBy(
      asc(crmDocumentTemplateTableColumns.sortOrder),
      asc(crmDocumentTemplateTableColumns.createdAt)
    );
  const columnMap = new Map<string, DocumentTemplateTableColumnRecord[]>();

  for (const column of columns.map(mapTableColumnRecord)) {
    const items = columnMap.get(column.mappingId) ?? [];
    items.push(column);
    columnMap.set(column.mappingId, items);
  }

  return mappings.map((mapping) => ({
    ...mapMappingRecord(mapping),
    columns: columnMap.get(mapping.id) ?? []
  }));
}

export async function listDocumentTemplates(
  organizationId: string,
  filters: DocumentTemplateFilters = {}
): Promise<DocumentTemplateListItem[]> {
  const templates = await db
    .select()
    .from(crmDocumentTemplates)
    .where(
      and(
        eq(crmDocumentTemplates.organizationId, organizationId),
        isNull(crmDocumentTemplates.deletedAt),
        ...(filters.documentType ? [eq(crmDocumentTemplates.documentType, filters.documentType)] : []),
        ...(filters.fileType ? [eq(crmDocumentTemplates.fileType, filters.fileType)] : []),
        ...(filters.isActive === 'true' ? [eq(crmDocumentTemplates.isActive, true)] : []),
        ...(filters.isActive === 'false' ? [eq(crmDocumentTemplates.isActive, false)] : [])
      )
    )
    .orderBy(asc(crmDocumentTemplates.templateName));
  const versions = await db
    .select()
    .from(crmDocumentTemplateVersions)
    .where(
      and(
        eq(crmDocumentTemplateVersions.organizationId, organizationId),
        isNull(crmDocumentTemplateVersions.deletedAt)
      )
    )
    .orderBy(asc(crmDocumentTemplateVersions.createdAt));
  const mappings = await db
    .select()
    .from(crmDocumentTemplateMappings)
    .where(
      and(
        eq(crmDocumentTemplateMappings.organizationId, organizationId),
        isNull(crmDocumentTemplateMappings.deletedAt)
      )
    );

  return templates.map((template) => {
    const templateVersions = versions.filter((item) => item.templateId === template.id);
    const latestVersion = templateVersions.at(-1) ?? null;
    const templateVersionIds = new Set(templateVersions.map((item) => item.id));
    const mappingCount = mappings.filter((item) => templateVersionIds.has(item.templateVersionId)).length;

    return {
      ...mapTemplateRecord(template),
      latestVersion: latestVersion?.version ?? null,
      versionCount: templateVersions.length,
      mappingCount
    };
  });
}

export async function getDocumentTemplate(
  id: string,
  organizationId: string
): Promise<DocumentTemplateDetail> {
  const template = await assertTemplate(id, organizationId);
  const versions = await db
    .select()
    .from(crmDocumentTemplateVersions)
    .where(
      and(
        eq(crmDocumentTemplateVersions.templateId, id),
        eq(crmDocumentTemplateVersions.organizationId, organizationId),
        isNull(crmDocumentTemplateVersions.deletedAt)
      )
    )
    .orderBy(asc(crmDocumentTemplateVersions.createdAt));
  const versionsWithMappings: DocumentTemplateVersionDetail[] = [];
  let mappingCount = 0;

  for (const version of versions) {
    const mappings = await resolveTemplateMappings(version.id, organizationId);
    mappingCount += mappings.length;
    versionsWithMappings.push({
      ...mapVersionRecord(version),
      mappings
    });
  }

  return {
    ...mapTemplateRecord(template),
    versions: versionsWithMappings,
    mappingCount
  };
}

export async function listDocumentTemplateVersions(id: string, organizationId: string) {
  await assertTemplate(id, organizationId);
  const versions = await db
    .select()
    .from(crmDocumentTemplateVersions)
    .where(
      and(
        eq(crmDocumentTemplateVersions.templateId, id),
        eq(crmDocumentTemplateVersions.organizationId, organizationId),
        isNull(crmDocumentTemplateVersions.deletedAt)
      )
    )
    .orderBy(asc(crmDocumentTemplateVersions.createdAt));

  return Promise.all(
    versions.map(async (version) => ({
      ...mapVersionRecord(version),
      mappings: await resolveTemplateMappings(version.id, organizationId)
    }))
  );
}

export async function createDocumentTemplate(
  organizationId: string,
  userId: string,
  payload: DocumentTemplateMutationPayload
) {
  const [created] = await db
    .insert(crmDocumentTemplates)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      documentType: payload.documentType,
      productType: payload.productType,
      fileType: payload.fileType,
      templateName: payload.templateName.trim(),
      description: payload.description?.trim() || null,
      isDefault: payload.isDefault ?? false,
      isActive: payload.isActive ?? true,
      createdBy: userId,
      updatedBy: userId
    })
    .returning();

  if (created.isDefault) {
    await normalizeDefaultTemplate(created.id, organizationId, created.documentType, created.fileType);
  }

  return created;
}

export async function updateDocumentTemplate(
  id: string,
  organizationId: string,
  userId: string,
  payload: Partial<DocumentTemplateMutationPayload>
) {
  const template = await assertTemplate(id, organizationId);
  const [updated] = await db
    .update(crmDocumentTemplates)
    .set({
      documentType: payload.documentType ?? template.documentType,
      productType: payload.productType ?? template.productType,
      fileType: payload.fileType ?? template.fileType,
      templateName: payload.templateName?.trim() ?? template.templateName,
      description: payload.description === undefined ? template.description : payload.description?.trim() || null,
      isDefault: payload.isDefault ?? template.isDefault,
      isActive: payload.isActive ?? template.isActive,
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmDocumentTemplates.id, id))
    .returning();

  if (updated.isDefault) {
    await normalizeDefaultTemplate(updated.id, organizationId, updated.documentType, updated.fileType);
  }

  return updated;
}

async function normalizeDefaultTemplate(
  templateId: string,
  organizationId: string,
  documentType: string,
  fileType: string
) {
  await db
    .update(crmDocumentTemplates)
    .set({
      isDefault: false,
      updatedAt: new Date()
    })
    .where(
      and(
        eq(crmDocumentTemplates.organizationId, organizationId),
        eq(crmDocumentTemplates.documentType, documentType),
        eq(crmDocumentTemplates.fileType, fileType),
        isNull(crmDocumentTemplates.deletedAt)
      )
    );

  await db
    .update(crmDocumentTemplates)
    .set({
      isDefault: true,
      updatedAt: new Date()
    })
    .where(eq(crmDocumentTemplates.id, templateId));
}

export async function softDeleteDocumentTemplate(id: string, organizationId: string, userId: string) {
  await assertTemplate(id, organizationId);
  const [updated] = await db
    .update(crmDocumentTemplates)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmDocumentTemplates.id, id))
    .returning();

  return updated;
}

export async function createDocumentTemplateVersion(
  templateId: string,
  organizationId: string,
  userId: string,
  payload: DocumentTemplateVersionMutationPayload
) {
  await assertTemplate(templateId, organizationId);
  const [created] = await db
    .insert(crmDocumentTemplateVersions)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      templateId,
      version: payload.version.trim(),
      filePath: payload.filePath?.trim() || null,
      schemaJson: payload.schemaJson,
      previewImageUrl: payload.previewImageUrl?.trim() || null,
      isActive: payload.isActive ?? true,
      createdBy: userId
    })
    .returning();

  return created;
}

export async function updateDocumentTemplateVersion(
  versionId: string,
  organizationId: string,
  payload: Partial<DocumentTemplateVersionMutationPayload>
) {
  const current = await assertTemplateVersion(versionId, organizationId);
  const [updated] = await db
    .update(crmDocumentTemplateVersions)
    .set({
      version: payload.version?.trim() ?? current.version,
      filePath: payload.filePath === undefined ? current.filePath : payload.filePath?.trim() || null,
      schemaJson: payload.schemaJson ?? current.schemaJson,
      previewImageUrl:
        payload.previewImageUrl === undefined
          ? current.previewImageUrl
          : payload.previewImageUrl?.trim() || null,
      isActive: payload.isActive ?? current.isActive,
      updatedAt: new Date()
    })
    .where(eq(crmDocumentTemplateVersions.id, versionId))
    .returning();

  return updated;
}

export async function setDocumentTemplateVersionActive(
  versionId: string,
  organizationId: string,
  isActive: boolean
) {
  const version = await assertTemplateVersion(versionId, organizationId);
  const [updated] = await db
    .update(crmDocumentTemplateVersions)
    .set({
      isActive,
      updatedAt: new Date()
    })
    .where(eq(crmDocumentTemplateVersions.id, versionId))
    .returning();

  return {
    before: version,
    after: updated
  };
}

export async function createDocumentTemplateMapping(
  versionId: string,
  organizationId: string,
  payload: DocumentTemplateMappingMutationPayload
) {
  await assertTemplateVersion(versionId, organizationId);
  const [created] = await db
    .insert(crmDocumentTemplateMappings)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      templateVersionId: versionId,
      placeholderKey: payload.placeholderKey.trim(),
      sourcePath: payload.sourcePath.trim(),
      dataType: payload.dataType,
      sheetName: payload.sheetName?.trim() || null,
      defaultValue: payload.defaultValue?.trim() || null,
      formatMask: payload.formatMask?.trim() || null,
      sortOrder: payload.sortOrder ?? 0
    })
    .returning();

  if (payload.columns?.length) {
    await db.insert(crmDocumentTemplateTableColumns).values(
      payload.columns.map((column) => ({
        id: crypto.randomUUID(),
        organizationId,
        mappingId: created.id,
        columnName: column.columnName.trim(),
        sourceField: column.sourceField.trim(),
        columnLetter: column.columnLetter?.trim() || null,
        sortOrder: column.sortOrder ?? 0,
        formatMask: column.formatMask?.trim() || null
      }))
    );
  }

  const [mapping] = await resolveTemplateMappings(versionId, organizationId).then((items) =>
    items.filter((item) => item.id === created.id)
  );

  return mapping ?? { ...mapMappingRecord(created), columns: [] };
}

export async function updateDocumentTemplateMapping(
  mappingId: string,
  organizationId: string,
  payload: Partial<DocumentTemplateMappingMutationPayload>
) {
  const current = await assertTemplateMapping(mappingId, organizationId);
  const [updated] = await db
    .update(crmDocumentTemplateMappings)
    .set({
      placeholderKey: payload.placeholderKey?.trim() ?? current.placeholderKey,
      sourcePath: payload.sourcePath?.trim() ?? current.sourcePath,
      dataType: payload.dataType ?? current.dataType,
      sheetName: payload.sheetName === undefined ? current.sheetName : payload.sheetName?.trim() || null,
      defaultValue:
        payload.defaultValue === undefined ? current.defaultValue : payload.defaultValue?.trim() || null,
      formatMask:
        payload.formatMask === undefined ? current.formatMask : payload.formatMask?.trim() || null,
      sortOrder: payload.sortOrder ?? current.sortOrder,
      updatedAt: new Date()
    })
    .where(eq(crmDocumentTemplateMappings.id, mappingId))
    .returning();

  if (payload.columns) {
    await db
      .delete(crmDocumentTemplateTableColumns)
      .where(eq(crmDocumentTemplateTableColumns.mappingId, mappingId));

    if (payload.columns.length) {
      await db.insert(crmDocumentTemplateTableColumns).values(
        payload.columns.map((column) => ({
          id: crypto.randomUUID(),
          organizationId,
          mappingId,
          columnName: column.columnName.trim(),
          sourceField: column.sourceField.trim(),
          columnLetter: column.columnLetter?.trim() || null,
          sortOrder: column.sortOrder ?? 0,
          formatMask: column.formatMask?.trim() || null
        }))
      );
    }
  }

  const [mapping] = await resolveTemplateMappings(updated.templateVersionId, organizationId).then((items) =>
    items.filter((item) => item.id === mappingId)
  );

  return mapping ?? { ...mapMappingRecord(updated), columns: [] };
}

export async function deleteDocumentTemplateMapping(mappingId: string, organizationId: string) {
  const current = await assertTemplateMapping(mappingId, organizationId);

  await db
    .delete(crmDocumentTemplateTableColumns)
    .where(eq(crmDocumentTemplateTableColumns.mappingId, mappingId));

  const [deleted] = await db
    .update(crmDocumentTemplateMappings)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date()
    })
    .where(eq(crmDocumentTemplateMappings.id, mappingId))
    .returning();

  return {
    before: current,
    after: deleted
  };
}

export async function resolveTemplateForDocument(
  organizationId: string,
  params: ResolveTemplateParams
): Promise<ResolvedDocumentTemplate> {
  const templates = await db
    .select()
    .from(crmDocumentTemplates)
    .where(
      and(
        eq(crmDocumentTemplates.organizationId, organizationId),
        eq(crmDocumentTemplates.documentType, params.documentType),
        eq(crmDocumentTemplates.fileType, params.fileType),
        isNull(crmDocumentTemplates.deletedAt),
        eq(crmDocumentTemplates.isActive, true)
      )
    )
    .orderBy(asc(crmDocumentTemplates.createdAt));
  const exactTemplate =
    templates.find(
      (template) => template.productType === (params.productType ?? 'default') && template.isDefault
    ) ??
    templates.find((template) => template.productType === (params.productType ?? 'default')) ??
    templates.find((template) => template.productType === 'default' && template.isDefault) ??
    templates.find((template) => template.productType === 'default') ??
    templates.find((template) => template.isDefault) ??
    templates[0];

  if (!exactTemplate) {
    throw new AuthError('No active document template found', 404);
  }

  const versions = await db
    .select()
    .from(crmDocumentTemplateVersions)
    .where(
      and(
        eq(crmDocumentTemplateVersions.organizationId, organizationId),
        eq(crmDocumentTemplateVersions.templateId, exactTemplate.id),
        eq(crmDocumentTemplateVersions.isActive, true),
        isNull(crmDocumentTemplateVersions.deletedAt)
      )
    )
    .orderBy(asc(crmDocumentTemplateVersions.createdAt));
  const version = versions.at(-1);

  if (!version) {
    throw new AuthError('No active template version found', 404);
  }

  return {
    template: mapTemplateRecord(exactTemplate),
    version: mapVersionRecord(version)
  };
}

function getValueByPath(source: unknown, path: string): unknown {
  return path.split('.').reduce<unknown>((current, segment) => {
    if (current === null || current === undefined) {
      return undefined;
    }

    if (Array.isArray(current) && /^\d+$/.test(segment)) {
      return current[Number(segment)];
    }

    if (typeof current === 'object' && current !== null && segment in current) {
      return (current as Record<string, unknown>)[segment];
    }

    return undefined;
  }, source);
}

function applyFormatMask(value: unknown, formatMask?: string | null) {
  if (value === null || value === undefined) {
    return value;
  }

  if (formatMask === 'currency' || formatMask?.startsWith('currency_')) {
    const currencyCode =
      formatMask === 'currency' ? 'THB' : formatMask.replace('currency_', '');
    return formatPdfCurrency(value as number | string, currencyCode);
  }

  if (formatMask === 'date' || formatMask === 'date_en') {
    return formatPdfDate(value as string, 'en');
  }

  if (formatMask === 'date_th') {
    return formatPdfDate(value as string, 'th');
  }

  return value;
}

export function mapDocumentDataToTemplateInput(
  documentData: Record<string, unknown>,
  mappings: DocumentTemplateMappingWithColumns[]
) {
  const result: Record<string, unknown> = {};

  for (const mapping of mappings) {
    const rawValue = getValueByPath(documentData, mapping.sourcePath);

    if (mapping.dataType === 'table') {
      const rows = Array.isArray(rawValue) ? rawValue : [];

      if (rows.length === 0) {
        result[mapping.placeholderKey] = normalizePdfmeTable([]);
        continue;
      }

      if (rows.every((row) => Array.isArray(row))) {
        result[mapping.placeholderKey] = normalizePdfmeTable(rows);
        continue;
      }

      if (mapping.columns.length) {
        const normalizedRows = rows.map((row) => {
          const rowRecord = row as Record<string, unknown>;

          return mapping.columns.map((column) => {
            const formattedValue = applyFormatMask(rowRecord[column.sourceField], column.formatMask);
            return String(formattedValue ?? '-').trim() || '-';
          });
        });

        result[mapping.placeholderKey] = normalizePdfmeTable(normalizedRows);
        continue;
      }

      result[mapping.placeholderKey] = normalizePdfmeTable(rows);
      continue;
    }

    if (mapping.dataType === 'multiline') {
      if (Array.isArray(rawValue)) {
        const normalized = rawValue
          .map((item) => {
            if (typeof item === 'string') {
              return item;
            }

            if (item && typeof item === 'object' && 'items' in item) {
              return ((item as { items?: string[] }).items ?? []).join('\n');
            }

            return String(item ?? '');
          })
          .filter(Boolean)
          .join('\n\n');
        result[mapping.placeholderKey] = normalized || mapping.defaultValue || '';
        continue;
      }

      result[mapping.placeholderKey] =
        typeof rawValue === 'string' ? rawValue : mapping.defaultValue || '';
      continue;
    }

    result[mapping.placeholderKey] =
      applyFormatMask(rawValue, mapping.formatMask) ?? mapping.defaultValue ?? '-';
  }

  return result;
}
