import { and, asc, eq, sql } from 'drizzle-orm';
import { documentSequences } from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { getCurrentOrganization } from '@/features/foundation/organization-context/service';
import type {
  DocumentSequenceInput,
  DocumentSequenceListItem,
  DocumentSequenceMutationPayload,
  DocumentSequenceRecord,
  DocumentSequenceResetPayload,
  DocumentSequenceResult
} from './types';

const DEFAULT_DOCUMENT_PREFIXES: Record<string, string> = {
  customer: 'CUS',
  contact: 'CON',
  enquiry: 'ENQ',
  quotation: 'QT',
  approval: 'APV'
};

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 buildDocumentCode(
  prefix: string,
  period: string,
  nextNumber: number,
  paddingLength: number
) {
  return `${prefix}${period}-${String(nextNumber).padStart(paddingLength, '0')}`;
}

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

function mapSequenceRecord(row: typeof documentSequences.$inferSelect): DocumentSequenceRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    branchId: row.branchId || null,
    documentType: row.documentType,
    prefix: row.prefix,
    period: row.period,
    currentNumber: row.currentNumber,
    paddingLength: row.paddingLength,
    isActive: row.isActive,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString()
  };
}

function mapSequenceListItem(row: typeof documentSequences.$inferSelect): DocumentSequenceListItem {
  return {
    ...mapSequenceRecord(row),
    nextPreview: buildDocumentCode(
      row.prefix,
      row.period,
      row.currentNumber + 1,
      row.paddingLength
    )
  };
}

async function resolveOrganizationId(organizationId?: string) {
  if (organizationId) {
    return organizationId;
  }

  const organization = await getCurrentOrganization();

  if (!organization) {
    throw new Error('Active organization is required');
  }

  return organization.id;
}

async function assertSequence(id: string, organizationId: string) {
  const [sequence] = await db
    .select()
    .from(documentSequences)
    .where(
      and(eq(documentSequences.id, id), eq(documentSequences.organizationId, organizationId))
    )
    .limit(1);

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

  return sequence;
}

async function ensureSequence(
  organizationId: string,
  documentType: string,
  period: string,
  branchId: string
) {
  const existing = await db.query.documentSequences.findFirst({
    where: and(
      eq(documentSequences.organizationId, organizationId),
      eq(documentSequences.documentType, documentType),
      eq(documentSequences.period, period),
      eq(documentSequences.branchId, branchId)
    )
  });

  if (existing) {
    return existing;
  }

  const prefix = DEFAULT_DOCUMENT_PREFIXES[documentType] ?? documentType.slice(0, 3).toUpperCase();

  const [created] = await db
    .insert(documentSequences)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      branchId,
      documentType,
      period,
      prefix,
      currentNumber: 0,
      paddingLength: 3,
      isActive: true
    })
    .returning();

  return created;
}

function toSequenceResult(row: typeof documentSequences.$inferSelect): DocumentSequenceResult {
  const nextNumber = row.currentNumber + 1;

  return {
    code: buildDocumentCode(row.prefix, row.period, nextNumber, row.paddingLength),
    documentType: row.documentType,
    branchId: row.branchId || null,
    currentNumber: row.currentNumber,
    nextNumber,
    period: row.period,
    prefix: row.prefix
  };
}

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

  return rows.map(mapSequenceListItem);
}

export async function getDocumentSequence(id: string, organizationId: string): Promise<DocumentSequenceRecord> {
  const row = await assertSequence(id, organizationId);
  return mapSequenceRecord(row);
}

export async function createDocumentSequence(
  organizationId: string,
  payload: DocumentSequenceMutationPayload
): Promise<DocumentSequenceRecord> {
  const branchId = normalizeBranchId(payload.branchId);
  const [created] = await db
    .insert(documentSequences)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      branchId,
      documentType: payload.documentType.trim(),
      prefix: payload.prefix.trim(),
      period: payload.period.trim(),
      currentNumber: payload.currentNumber ?? 0,
      paddingLength: payload.paddingLength,
      isActive: payload.isActive ?? true,
      updatedAt: new Date()
    })
    .returning();

  return mapSequenceRecord(created);
}

export async function updateDocumentSequence(
  id: string,
  organizationId: string,
  payload: Partial<DocumentSequenceMutationPayload>
): Promise<DocumentSequenceRecord> {
  const current = await assertSequence(id, organizationId);
  const [updated] = await db
    .update(documentSequences)
    .set({
      documentType: payload.documentType?.trim() ?? current.documentType,
      prefix: payload.prefix?.trim() ?? current.prefix,
      period: payload.period?.trim() ?? current.period,
      branchId:
        payload.branchId === undefined ? current.branchId : normalizeBranchId(payload.branchId),
      currentNumber: payload.currentNumber ?? current.currentNumber,
      paddingLength: payload.paddingLength ?? current.paddingLength,
      isActive: payload.isActive ?? current.isActive,
      updatedAt: new Date()
    })
    .where(eq(documentSequences.id, id))
    .returning();

  return mapSequenceRecord(updated);
}

export async function resetDocumentSequence(
  id: string,
  organizationId: string,
  payload: DocumentSequenceResetPayload
): Promise<DocumentSequenceRecord> {
  await assertSequence(id, organizationId);
  const [updated] = await db
    .update(documentSequences)
    .set({
      currentNumber: payload.currentNumber,
      updatedAt: new Date()
    })
    .where(eq(documentSequences.id, id))
    .returning();

  return mapSequenceRecord(updated);
}

export async function deleteDocumentSequence(id: string, organizationId: string): Promise<DocumentSequenceRecord> {
  await assertSequence(id, organizationId);
  const [deleted] = await db
    .delete(documentSequences)
    .where(eq(documentSequences.id, id))
    .returning();

  return mapSequenceRecord(deleted);
}

export async function previewDocumentSequenceById(
  id: string,
  organizationId: string
): Promise<DocumentSequenceResult> {
  const sequence = await assertSequence(id, organizationId);
  return toSequenceResult(sequence);
}

export async function previewNextDocumentCode(
  input: DocumentSequenceInput
): Promise<DocumentSequenceResult> {
  const organizationId = await resolveOrganizationId(input.organizationId);
  const period = input.period ?? getCurrentPeriod();
  const branchId = normalizeBranchId(input.branchId);
  const sequence = await ensureSequence(organizationId, input.documentType, period, branchId);

  return toSequenceResult(sequence);
}

export async function generateNextDocumentCode(
  input: DocumentSequenceInput
): Promise<DocumentSequenceResult> {
  const organizationId = await resolveOrganizationId(input.organizationId);
  const period = input.period ?? getCurrentPeriod();
  const branchId = normalizeBranchId(input.branchId);

  await ensureSequence(organizationId, input.documentType, period, branchId);

  return db.transaction(async (tx) => {
    await tx.execute(sql`
      select id
      from document_sequences
      where organization_id = ${organizationId}
        and document_type = ${input.documentType}
        and period = ${period}
        and branch_id = ${branchId}
      for update
    `);

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

    if (!sequence) {
      throw new Error('Document sequence not found');
    }

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

    return {
      code: buildDocumentCode(
        updated.prefix,
        updated.period,
        updated.currentNumber,
        updated.paddingLength
      ),
      documentType: updated.documentType,
      branchId: updated.branchId || null,
      currentNumber: updated.currentNumber,
      nextNumber: updated.currentNumber + 1,
      period: updated.period,
      prefix: updated.prefix
    };
  });
}
