import { and, asc, eq, ne, sql } from "drizzle-orm";
import { documentSequences, organizations } from "@/db/schema";
import { auditAction } from "@/features/foundation/audit-log/service";
import { resolveBranchDisplays } from "@/features/foundation/display/server/display-resolver";
import { getActiveOptionsByCategory } from "@/features/foundation/master-options/service";
import { getCurrentOrganization } from "@/features/foundation/organization-context/service";
import { db } from "@/lib/db";
import { AuthError } from "@/lib/auth/session";
import {
  DOCUMENT_SEQUENCE_DEFAULT_FORMAT,
  DOCUMENT_SEQUENCE_DEFAULT_RESET_POLICY,
  DOCUMENT_SEQUENCE_GENERIC_PRODUCT_TYPE,
  normalizeDocumentSequenceProductType,
  resolveDocumentSequenceOrganizationCode,
  resolveDocumentSequenceProductTypeCode,
} from "./config";
import {
  resolveDocumentSequenceProductTypeInput,
  type DocumentSequenceProductTypeLookupOption,
} from "./product-type-resolver";
import type {
  DocumentSequenceInput,
  DocumentSequenceListItem,
  DocumentSequenceMutationPayload,
  DocumentSequenceRecord,
  DocumentSequenceResetPayload,
  DocumentSequenceResult,
} from "./types";

const PRODUCT_TYPE_CATEGORY = "crm_product_type";
const QUOTATION_TYPE_CATEGORY = "crm_quotation_type";

const DEFAULT_DOCUMENT_PREFIXES: Record<string, string> = {
  customer: "CUS",
  contact: "CON",
  opportunity: "ENQ",
  crm_lead: "LD",
  crm_opportunity: "EN",
  quotation: "QT",
  approval: "APV",
};

type OrganizationRecord = {
  id: string;
  name: string;
  slug: string;
};

function getCurrentPeriod(date = new Date()) {
  const year = String(date.getFullYear()).slice(-2);
  const month = String(date.getMonth() + 1).padStart(2, "0");
  return `${year}${month}`;
}

function normalizeBranchId(branchId?: string | null) {
  return branchId?.trim() || "";
}

function normalizeDocumentType(documentType: string) {
  const normalized = documentType.trim();

  if (!normalized) {
    throw new AuthError("Document type is required.", 400);
  }

  return normalized;
}

function normalizePeriod(period?: string | null) {
  const normalized = period?.trim();

  if (!normalized) {
    throw new AuthError("Period is required.", 400);
  }

  if (!/^\d{4}$/.test(normalized)) {
    throw new AuthError("Period must use YYMM format.", 400);
  }

  return normalized;
}

function normalizeFormat(value?: string | null) {
  return value?.trim() || DOCUMENT_SEQUENCE_DEFAULT_FORMAT;
}

function normalizeResetPolicy(value?: string | null) {
  return value?.trim() || DOCUMENT_SEQUENCE_DEFAULT_RESET_POLICY;
}

function formatBranchDisplayName(input: {
  branchId: string | null;
  branchCode?: string | null;
  branchName?: string | null;
}) {
  if (!input.branchId) {
    return "All Branches";
  }

  if (input.branchCode && input.branchName) {
    return `${input.branchCode} - ${input.branchName}`;
  }

  if (input.branchName) {
    return input.branchName;
  }

  return `Unknown Branch (${input.branchId})`;
}

function buildDocumentCode(
  format: string,
  prefix: string,
  period: string,
  nextNumber: number,
  paddingLength: number,
) {
  const running = String(nextNumber).padStart(paddingLength, "0");

  return format
    .replaceAll("{prefix}", prefix)
    .replaceAll("{period}", period)
    .replaceAll("{running}", running);
}

async function resolveOrganizationRecord(
  organizationId?: string,
): Promise<OrganizationRecord> {
  if (!organizationId) {
    const organization = await getCurrentOrganization();

    if (!organization) {
      throw new AuthError("Active organization required.", 400);
    }

    return {
      id: organization.id,
      name: organization.name,
      slug: organization.slug,
    };
  }

  const [organization] = await db
    .select({
      id: organizations.id,
      name: organizations.name,
      slug: organizations.slug,
    })
    .from(organizations)
    .where(eq(organizations.id, organizationId))
    .limit(1);

  if (!organization) {
    throw new AuthError("Organization not found.", 404);
  }

  return organization;
}

async function resolveOptionMap(category: string, organizationId: string) {
  const options = await getActiveOptionsByCategory(category, { organizationId });

  return new Map(
    options.map((option) => [
      normalizeDocumentSequenceProductType(option.code),
      {
        id: option.id,
        code: normalizeDocumentSequenceProductType(option.code),
        label: option.label,
      },
    ]),
  );
}

async function resolveProductTypeMap(organizationId: string) {
  return resolveOptionMap(PRODUCT_TYPE_CATEGORY, organizationId);
}

async function resolveQuotationTypeMap(organizationId: string) {
  return resolveOptionMap(QUOTATION_TYPE_CATEGORY, organizationId);
}

function buildProductTypeResolutionDetails(input: {
  organizationId: string;
  documentType: string;
  requestedProductType: string | null;
  normalizedRequestedProductType: string | null;
  productTypeOptions: ReadonlyArray<DocumentSequenceProductTypeLookupOption>;
  referenceOptions: ReadonlyArray<DocumentSequenceProductTypeLookupOption>;
}) {
  return {
    organizationId: input.organizationId,
    documentType: input.documentType,
    requestedProductType: input.requestedProductType,
    normalizedRequestedProductType: input.normalizedRequestedProductType,
    productTypeOptionIds: input.productTypeOptions.map((option) => option.id),
    productTypeOptionCodes: input.productTypeOptions.map((option) => option.code),
    referenceOptionIds: input.referenceOptions.map((option) => option.id),
    referenceOptionCodes: input.referenceOptions.map((option) => option.code),
  };
}

function buildProductTypeResolutionError(input: {
  organizationId: string;
  documentType: string;
  requestedProductType: string | null;
  normalizedRequestedProductType: string | null;
  productTypeOptions: ReadonlyArray<DocumentSequenceProductTypeLookupOption>;
  referenceOptions: ReadonlyArray<DocumentSequenceProductTypeLookupOption>;
}) {
  const error = new AuthError(
    "Missing product type code configuration for quotation type.",
    400,
  ) as AuthError & {
    details: ReturnType<typeof buildProductTypeResolutionDetails>;
  };

  error.details = buildProductTypeResolutionDetails(input);
  return error;
}

async function resolveScopedProductType(input: {
  organizationId: string;
  documentType: string;
  productType?: string | null;
}) {
  const normalizedDocumentType = normalizeDocumentType(input.documentType);
  const requestedProductType = input.productType?.trim() ?? null;

  if (normalizedDocumentType !== "quotation") {
    return requestedProductType
      ? normalizeDocumentSequenceProductType(requestedProductType)
      : DOCUMENT_SEQUENCE_GENERIC_PRODUCT_TYPE;
  }

  if (!requestedProductType) {
    throw new AuthError(
      "Product type required for quotation document sequences.",
      400,
    );
  }

  const productTypeOptions = [
    ...(await resolveProductTypeMap(input.organizationId)).values(),
  ];
  const referenceOptions = [
    ...(await resolveQuotationTypeMap(input.organizationId)).values(),
  ];
  const resolution = resolveDocumentSequenceProductTypeInput({
    documentType: normalizedDocumentType,
    productType: requestedProductType,
    options: productTypeOptions,
    referenceOptions,
  });

  if (resolution.productType) {
    return resolution.productType;
  }

  throw buildProductTypeResolutionError({
    organizationId: input.organizationId,
    documentType: normalizedDocumentType,
    requestedProductType,
    normalizedRequestedProductType: resolution.normalizedRequestedProductType,
    productTypeOptions,
    referenceOptions,
  });
}

async function resolvePrefix(input: {
  organization: OrganizationRecord;
  documentType: string;
  productType: string;
  prefix?: string | null;
}) {
  const manualPrefix = input.prefix?.trim();
  if (manualPrefix) {
    return manualPrefix;
  }

  if (input.documentType !== "quotation") {
    return (
      DEFAULT_DOCUMENT_PREFIXES[input.documentType] ??
      input.documentType.slice(0, 3).toUpperCase()
    );
  }

  const organizationCode = resolveDocumentSequenceOrganizationCode(
    input.organization.slug,
  );
  if (!organizationCode) {
    throw new AuthError(
      `Missing organization code configuration for ${input.organization.slug}.`,
      400,
    );
  }

  const productTypeCode = resolveDocumentSequenceProductTypeCode(
    input.productType,
  );
  if (!productTypeCode) {
    throw new AuthError(
      `Missing product type code configuration for ${input.productType}.`,
      400,
    );
  }

  return `${productTypeCode}${organizationCode}`;
}

async function assertSequence(id: string, organizationId: string) {
  const row = await db.query.documentSequences.findFirst({
    where: and(
      eq(documentSequences.id, id),
      eq(documentSequences.organizationId, organizationId),
    ),
  });

  if (!row) {
    throw new AuthError("Document sequence not found.", 404);
  }

  return row;
}

async function assertUniqueScope(input: {
  organizationId: string;
  branchId: string;
  productType: string;
  documentType: string;
  period: string;
  currentId?: string;
}) {
  const existing = await db.query.documentSequences.findFirst({
    where: and(
      eq(documentSequences.organizationId, input.organizationId),
      eq(documentSequences.branchId, input.branchId),
      eq(documentSequences.productType, input.productType),
      eq(documentSequences.documentType, input.documentType),
      eq(documentSequences.period, input.period),
      ...(input.currentId ? [ne(documentSequences.id, input.currentId)] : []),
    ),
  });

  if (existing) {
    throw new AuthError(
      "Duplicate document sequence scope for organization, branch, product type, document type, and period.",
      409,
    );
  }
}

async function mapSequenceRecords(
  organization: OrganizationRecord,
  rows: Array<typeof documentSequences.$inferSelect>,
) {
  const [branchDisplayMap, productTypeMap] = await Promise.all([
    resolveBranchDisplays({
      organizationId: organization.id,
      branchIds: rows.map((row) => row.branchId || null),
    }),
    resolveProductTypeMap(organization.id),
  ]);

  const organizationCode = resolveDocumentSequenceOrganizationCode(
    organization.slug,
  );

  return rows.map((row) => {
    const branchId = row.branchId || null;
    const branchDisplay = branchId
      ? (branchDisplayMap.get(branchId) ?? null)
      : null;
    const normalizedProductType = normalizeDocumentSequenceProductType(
      row.productType,
    );
    const productType = productTypeMap.get(normalizedProductType) ?? null;

    return {
      id: row.id,
      organizationId: row.organizationId,
      organizationName: organization.name,
      organizationCode,
      branchId,
      branchCode: branchDisplay?.code ?? null,
      branchName: branchDisplay?.name ?? null,
      branchDisplayName: formatBranchDisplayName({
        branchId,
        branchCode: branchDisplay?.code ?? null,
        branchName: branchDisplay?.name ?? null,
      }),
      productType: normalizedProductType,
      productTypeLabel:
        normalizedProductType === DOCUMENT_SEQUENCE_GENERIC_PRODUCT_TYPE
          ? "All Product Types"
          : normalizedProductType === "legacy"
            ? "Legacy"
            : (productType?.label ?? null),
      productTypeSequenceCode:
        normalizedProductType === "legacy"
          ? null
          : resolveDocumentSequenceProductTypeCode(normalizedProductType),
      documentType: row.documentType,
      prefix: row.prefix,
      period: row.period,
      currentNumber: row.currentNumber,
      paddingLength: row.paddingLength,
      format: row.format,
      resetPolicy: row.resetPolicy,
      isActive: row.isActive,
      createdAt: row.createdAt.toISOString(),
      updatedAt: row.updatedAt.toISOString(),
    } satisfies DocumentSequenceRecord;
  });
}

function toSequenceResult(
  row: typeof documentSequences.$inferSelect,
): DocumentSequenceResult {
  return {
    code: buildDocumentCode(
      row.format,
      row.prefix,
      row.period,
      row.currentNumber + 1,
      row.paddingLength,
    ),
    documentType: row.documentType,
    productType: row.productType,
    branchId: row.branchId || null,
    currentNumber: row.currentNumber,
    nextNumber: row.currentNumber + 1,
    period: row.period,
    prefix: row.prefix,
  };
}

export async function listDocumentSequences(
  organizationId: string,
): Promise<DocumentSequenceListItem[]> {
  const organization = await resolveOrganizationRecord(organizationId);
  const rows = await db
    .select()
    .from(documentSequences)
    .where(eq(documentSequences.organizationId, organization.id))
    .orderBy(
      asc(documentSequences.documentType),
      asc(documentSequences.productType),
      asc(documentSequences.period),
      asc(documentSequences.branchId),
    );

  const records = await mapSequenceRecords(organization, rows);

  return records.map((record) => ({
    ...record,
    nextPreview: buildDocumentCode(
      record.format,
      record.prefix,
      record.period,
      record.currentNumber + 1,
      record.paddingLength,
    ),
  }));
}

export async function getDocumentSequence(
  id: string,
  organizationId: string,
): Promise<DocumentSequenceRecord> {
  const organization = await resolveOrganizationRecord(organizationId);
  const row = await assertSequence(id, organization.id);
  const [record] = await mapSequenceRecords(organization, [row]);

  return record;
}

export async function createDocumentSequence(
  organizationId: string,
  payload: DocumentSequenceMutationPayload,
): Promise<DocumentSequenceRecord> {
  const organization = await resolveOrganizationRecord(organizationId);
  const documentType = normalizeDocumentType(payload.documentType);
  const branchId = normalizeBranchId(payload.branchId);
  const productType = await resolveScopedProductType({
    organizationId: organization.id,
    documentType,
    productType: payload.productType,
  });
  const period = normalizePeriod(payload.period);

  await assertUniqueScope({
    organizationId: organization.id,
    branchId,
    productType,
    documentType,
    period,
  });

  const [created] = await db
    .insert(documentSequences)
    .values({
      id: crypto.randomUUID(),
      organizationId: organization.id,
      branchId,
      productType,
      documentType,
      prefix: await resolvePrefix({
        organization,
        documentType,
        productType,
        prefix: payload.prefix,
      }),
      period,
      currentNumber: payload.currentNumber ?? 0,
      paddingLength: payload.paddingLength,
      format: normalizeFormat(payload.format),
      resetPolicy: normalizeResetPolicy(payload.resetPolicy),
      isActive: payload.isActive ?? true,
      updatedAt: new Date(),
    })
    .returning();

  const [record] = await mapSequenceRecords(organization, [created]);
  return record;
}

export async function updateDocumentSequence(
  id: string,
  organizationId: string,
  payload: Partial<DocumentSequenceMutationPayload>,
): Promise<DocumentSequenceRecord> {
  const organization = await resolveOrganizationRecord(organizationId);
  const current = await assertSequence(id, organization.id);
  const documentType = normalizeDocumentType(
    payload.documentType ?? current.documentType,
  );
  const branchId =
    payload.branchId === undefined
      ? current.branchId
      : normalizeBranchId(payload.branchId);
  const productType = await resolveScopedProductType({
    organizationId: organization.id,
    documentType,
    productType: payload.productType ?? current.productType,
  });
  const period = normalizePeriod(payload.period ?? current.period);

  await assertUniqueScope({
    organizationId: organization.id,
    branchId,
    productType,
    documentType,
    period,
    currentId: id,
  });

  const [updated] = await db
    .update(documentSequences)
    .set({
      branchId,
      productType,
      documentType,
      prefix: await resolvePrefix({
        organization,
        documentType,
        productType,
        prefix: payload.prefix ?? current.prefix,
      }),
      period,
      currentNumber: payload.currentNumber ?? current.currentNumber,
      paddingLength: payload.paddingLength ?? current.paddingLength,
      format: normalizeFormat(payload.format ?? current.format),
      resetPolicy: normalizeResetPolicy(
        payload.resetPolicy ?? current.resetPolicy,
      ),
      isActive: payload.isActive ?? current.isActive,
      updatedAt: new Date(),
    })
    .where(eq(documentSequences.id, id))
    .returning();

  const [record] = await mapSequenceRecords(organization, [updated]);
  return record;
}

export async function resetDocumentSequence(
  id: string,
  organizationId: string,
  payload: DocumentSequenceResetPayload,
): Promise<DocumentSequenceRecord> {
  const organization = await resolveOrganizationRecord(organizationId);
  await assertSequence(id, organization.id);

  const [updated] = await db
    .update(documentSequences)
    .set({
      currentNumber: payload.currentNumber,
      updatedAt: new Date(),
    })
    .where(eq(documentSequences.id, id))
    .returning();

  const [record] = await mapSequenceRecords(organization, [updated]);
  return record;
}

export async function deleteDocumentSequence(
  id: string,
  organizationId: string,
) {
  const organization = await resolveOrganizationRecord(organizationId);
  const sequence = await assertSequence(id, organization.id);
  await db.delete(documentSequences).where(eq(documentSequences.id, id));
  const [record] = await mapSequenceRecords(organization, [sequence]);

  return record;
}

export async function previewDocumentSequenceById(
  id: string,
  organizationId: string,
) {
  const row = await assertSequence(id, organizationId);
  return toSequenceResult(row);
}

export async function generateNextDocumentCode(input: DocumentSequenceInput) {
  const organization = await resolveOrganizationRecord(input.organizationId);
  const documentType = normalizeDocumentType(input.documentType);
  const branchId = normalizeBranchId(input.branchId);
  const productType = await resolveScopedProductType({
    organizationId: organization.id,
    documentType,
    productType: input.productType,
  });
  const period = normalizePeriod(input.period ?? getCurrentPeriod());

  try {
    const result = await db.transaction(async (tx) => {
      let sequence = await tx.query.documentSequences.findFirst({
        where: and(
          eq(documentSequences.organizationId, organization.id),
          eq(documentSequences.branchId, branchId),
          eq(documentSequences.productType, productType),
          eq(documentSequences.documentType, documentType),
          eq(documentSequences.period, period),
        ),
      });

      if (!sequence) {
        await tx
          .insert(documentSequences)
          .values({
            id: crypto.randomUUID(),
            organizationId: organization.id,
            branchId,
            productType,
            documentType,
            prefix: await resolvePrefix({
              organization,
              documentType,
              productType,
            }),
            period,
            currentNumber: 0,
            paddingLength: 3,
            format: DOCUMENT_SEQUENCE_DEFAULT_FORMAT,
            resetPolicy: DOCUMENT_SEQUENCE_DEFAULT_RESET_POLICY,
            isActive: true,
          })
          .onConflictDoNothing();

        sequence = await tx.query.documentSequences.findFirst({
          where: and(
            eq(documentSequences.organizationId, organization.id),
            eq(documentSequences.branchId, branchId),
            eq(documentSequences.productType, productType),
            eq(documentSequences.documentType, documentType),
            eq(documentSequences.period, period),
          ),
        });
      }

      if (!sequence) {
        throw new AuthError("Unable to initialize document sequence.", 500);
      }

      if (!sequence.isActive) {
        throw new AuthError("Document sequence is inactive.", 400);
      }

      const [updated] = await tx
        .update(documentSequences)
        .set({
          currentNumber: sql`${documentSequences.currentNumber} + 1`,
          updatedAt: new Date(),
        })
        .where(eq(documentSequences.id, sequence.id))
        .returning();

      return {
        sequenceId: updated.id,
        result: {
          code: buildDocumentCode(
            updated.format,
            updated.prefix,
            updated.period,
            updated.currentNumber,
            updated.paddingLength,
          ),
          documentType: updated.documentType,
          productType: updated.productType,
          branchId: updated.branchId || null,
          currentNumber: updated.currentNumber,
          nextNumber: updated.currentNumber + 1,
          period: updated.period,
          prefix: updated.prefix,
        } satisfies DocumentSequenceResult,
      };
    });

    await auditAction({
      organizationId: organization.id,
      entityType: "crm_document_sequence",
      entityId: result.sequenceId,
      action: "generate",
      branchId: branchId || null,
      afterData: result.result,
    });

    return result.result;
  } catch (error) {
    try {
      const errorDetails =
        error && typeof error === "object" && "details" in error
          ? ((error as { details?: unknown }).details ?? null)
          : null;

      await auditAction({
        organizationId: organization.id,
        entityType: "crm_document_sequence",
        entityId: `${documentType}:${branchId}:${productType}:${period}`,
        action: "generate_failed",
        branchId: branchId || null,
        afterData: {
          message: error instanceof Error ? error.message : "Unknown error",
          documentType,
          productType,
          requestedProductType: input.productType?.trim() ?? null,
          period,
          details: errorDetails,
        },
      });
    } catch {
      // Keep original error as the primary failure signal.
    }

    throw error;
  }
}
