import { and, asc, count, desc, eq, inArray, isNull, sql } from 'drizzle-orm';
import {
  crmApprovalMatrices,
  crmApprovalRequests,
  crmApprovalSteps,
  crmApprovalWorkflows
} from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import type {
  CreateApprovalStepInput,
  ReorderApprovalStepsInput,
  UpdateApprovalStepInput
} from '../schemas/approval-step.schema';
import type {
  ApprovalWorkflowFiltersInput,
  CreateApprovalWorkflowInput,
  UpdateApprovalWorkflowInput
} from '../schemas/approval-workflow.schema';

export interface ApprovalWorkflowUsage {
  totalRequestCount: number;
  activeRequestCount: number;
  activeMatrixCount: number;
  isLocked: boolean;
  lockedReason: string | null;
  canEdit: boolean;
  canDelete: boolean;
  canDeactivate: boolean;
}

export interface ApprovalWorkflowRecord {
  id: string;
  organizationId: string;
  code: string;
  name: string;
  entityType: string;
  description: string | null;
  isSystem: boolean;
  isActive: boolean;
  createdBy: string;
  updatedBy: string;
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
}

export interface ApprovalStepRecord {
  id: string;
  organizationId: string;
  workflowId: string;
  stepNumber: number;
  roleCode: string;
  roleName: string;
  approvalMode: 'sequential' | 'any_one' | 'all_required';
  isRequired: boolean;
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
}

export interface ApprovalWorkflowListItem extends ApprovalWorkflowRecord {
  stepCount: number;
  usage: ApprovalWorkflowUsage;
}

export interface ApprovalWorkflowDetail extends ApprovalWorkflowRecord {
  steps: ApprovalStepRecord[];
  usage: ApprovalWorkflowUsage;
}

export interface ApprovalWorkflowListFilters {
  entityType?: string;
  activeOnly?: boolean;
}

function mapWorkflowRecord(row: typeof crmApprovalWorkflows.$inferSelect): ApprovalWorkflowRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    code: row.code,
    name: row.name,
    entityType: row.entityType,
    description: row.description ?? null,
    isSystem: row.isSystem,
    isActive: row.isActive,
    createdBy: row.createdBy,
    updatedBy: row.updatedBy,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

function mapStepRecord(row: typeof crmApprovalSteps.$inferSelect): ApprovalStepRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    workflowId: row.workflowId,
    stepNumber: row.stepNumber,
    roleCode: row.roleCode,
    roleName: row.roleName,
    approvalMode: row.approvalMode as ApprovalStepRecord['approvalMode'],
    isRequired: row.isRequired,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

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

  if (!workflow) {
    throw new AuthError('Approval workflow not found', 404);
  }

  return workflow;
}

async function assertStep(stepId: string, organizationId: string) {
  const [step] = await db
    .select()
    .from(crmApprovalSteps)
    .where(
      and(
        eq(crmApprovalSteps.id, stepId),
        eq(crmApprovalSteps.organizationId, organizationId),
        isNull(crmApprovalSteps.deletedAt)
      )
    )
    .limit(1);

  if (!step) {
    throw new AuthError('Approval step not found', 404);
  }

  return step;
}

async function listWorkflowStepsInternal(workflowId: string, organizationId: string) {
  return db
    .select()
    .from(crmApprovalSteps)
    .where(
      and(
        eq(crmApprovalSteps.workflowId, workflowId),
        eq(crmApprovalSteps.organizationId, organizationId),
        isNull(crmApprovalSteps.deletedAt)
      )
    )
    .orderBy(asc(crmApprovalSteps.stepNumber), asc(crmApprovalSteps.createdAt));
}

async function ensureWorkflowCodeUnique(
  organizationId: string,
  code: string,
  excludedWorkflowId?: string
) {
  const [existing] = await db
    .select({ id: crmApprovalWorkflows.id })
    .from(crmApprovalWorkflows)
    .where(
      and(
        eq(crmApprovalWorkflows.organizationId, organizationId),
        eq(crmApprovalWorkflows.code, code),
        isNull(crmApprovalWorkflows.deletedAt),
        ...(excludedWorkflowId ? [sql`${crmApprovalWorkflows.id} <> ${excludedWorkflowId}`] : [])
      )
    )
    .limit(1);

  if (existing) {
    throw new AuthError('Approval workflow code already exists', 409);
  }
}

async function getWorkflowUsageMap(workflowIds: string[], organizationId: string) {
  if (workflowIds.length === 0) {
    return new Map<string, ApprovalWorkflowUsage>();
  }

  const [allRequests, activeRequests, activeMatrices] = await Promise.all([
    db
      .select({
        workflowId: crmApprovalRequests.workflowId,
        count: count()
      })
      .from(crmApprovalRequests)
      .where(
        and(
          eq(crmApprovalRequests.organizationId, organizationId),
          inArray(crmApprovalRequests.workflowId, workflowIds),
          isNull(crmApprovalRequests.deletedAt)
        )
      )
      .groupBy(crmApprovalRequests.workflowId),
    db
      .select({
        workflowId: crmApprovalRequests.workflowId,
        count: count()
      })
      .from(crmApprovalRequests)
      .where(
        and(
          eq(crmApprovalRequests.organizationId, organizationId),
          inArray(crmApprovalRequests.workflowId, workflowIds),
          eq(crmApprovalRequests.status, 'pending'),
          isNull(crmApprovalRequests.deletedAt)
        )
      )
      .groupBy(crmApprovalRequests.workflowId),
    db
      .select({
        workflowId: crmApprovalMatrices.workflowId,
        count: count()
      })
      .from(crmApprovalMatrices)
      .where(
        and(
          eq(crmApprovalMatrices.organizationId, organizationId),
          inArray(crmApprovalMatrices.workflowId, workflowIds),
          eq(crmApprovalMatrices.isActive, true),
          isNull(crmApprovalMatrices.deletedAt)
        )
      )
      .groupBy(crmApprovalMatrices.workflowId)
  ]);

  const requestMap = new Map(allRequests.map((row) => [row.workflowId, Number(row.count)]));
  const activeRequestMap = new Map(activeRequests.map((row) => [row.workflowId, Number(row.count)]));
  const activeMatrixMap = new Map(activeMatrices.map((row) => [row.workflowId, Number(row.count)]));

  return new Map(
    workflowIds.map((workflowId) => {
      const totalRequestCount = requestMap.get(workflowId) ?? 0;
      const activeRequestCount = activeRequestMap.get(workflowId) ?? 0;
      const activeMatrixCount = activeMatrixMap.get(workflowId) ?? 0;
      const isLocked = totalRequestCount > 0;
      const lockedReason = isLocked
        ? 'This workflow has already been used by approval requests. Clone it before editing.'
        : null;

      return [
        workflowId,
        {
          totalRequestCount,
          activeRequestCount,
          activeMatrixCount,
          isLocked,
          lockedReason,
          canEdit: !isLocked,
          canDelete: totalRequestCount === 0 && activeMatrixCount === 0,
          canDeactivate: activeRequestCount === 0 && activeMatrixCount === 0
        } satisfies ApprovalWorkflowUsage
      ];
    })
  );
}

async function assertWorkflowEditable(workflowId: string, organizationId: string) {
  const usage = (await getWorkflowUsageMap([workflowId], organizationId)).get(workflowId);
  if (usage?.isLocked) {
    throw new AuthError(usage.lockedReason ?? 'Workflow is locked', 400);
  }
  return usage;
}

async function assertWorkflowCanDeactivate(workflowId: string, organizationId: string) {
  const usage = (await getWorkflowUsageMap([workflowId], organizationId)).get(workflowId);
  if (!usage) {
    throw new AuthError('Approval workflow not found', 404);
  }
  if (!usage.canDeactivate) {
    throw new AuthError(
      'Workflow cannot be deactivated while active matrices or pending approval requests still reference it',
      400
    );
  }
  return usage;
}

async function assertWorkflowCanDelete(workflowId: string, organizationId: string) {
  const usage = (await getWorkflowUsageMap([workflowId], organizationId)).get(workflowId);
  if (!usage) {
    throw new AuthError('Approval workflow not found', 404);
  }
  if (!usage.canDelete) {
    throw new AuthError(
      'Workflow cannot be deleted because it is used by approval requests or active approval matrices',
      400
    );
  }
  return usage;
}

async function assertWorkflowHasRequiredSteps(workflowId: string, organizationId: string) {
  const steps = await listWorkflowStepsInternal(workflowId, organizationId);
  if (steps.length === 0) {
    throw new AuthError('Active workflow must have at least one step', 400);
  }
  if (!steps.some((step) => step.isRequired)) {
    throw new AuthError('Active workflow must have at least one required step', 400);
  }
}

async function nextCloneCode(organizationId: string, baseCode: string) {
  const normalizedBase = `${baseCode}_copy`;
  const existingCodes = await db
    .select({ code: crmApprovalWorkflows.code })
    .from(crmApprovalWorkflows)
    .where(
      and(
        eq(crmApprovalWorkflows.organizationId, organizationId),
        isNull(crmApprovalWorkflows.deletedAt)
      )
    );

  const existing = new Set(existingCodes.map((row) => row.code));
  if (!existing.has(normalizedBase)) {
    return normalizedBase;
  }

  let index = 2;
  while (existing.has(`${normalizedBase}_${index}`)) {
    index += 1;
  }
  return `${normalizedBase}_${index}`;
}

export async function listApprovalWorkflows(
  organizationId: string,
  filters: ApprovalWorkflowListFilters = {}
): Promise<ApprovalWorkflowListItem[]> {
  const rows = await db
    .select()
    .from(crmApprovalWorkflows)
    .where(
      and(
        eq(crmApprovalWorkflows.organizationId, organizationId),
        isNull(crmApprovalWorkflows.deletedAt),
        ...(filters.entityType ? [eq(crmApprovalWorkflows.entityType, filters.entityType)] : []),
        ...(filters.activeOnly ? [eq(crmApprovalWorkflows.isActive, true)] : [])
      )
    )
    .orderBy(
      asc(crmApprovalWorkflows.entityType),
      desc(crmApprovalWorkflows.isActive),
      asc(crmApprovalWorkflows.name)
    );

  const workflowIds = rows.map((row) => row.id);
  const [steps, usageMap] = await Promise.all([
    workflowIds.length
      ? db
          .select({
            workflowId: crmApprovalSteps.workflowId
          })
          .from(crmApprovalSteps)
          .where(
            and(
              eq(crmApprovalSteps.organizationId, organizationId),
              inArray(crmApprovalSteps.workflowId, workflowIds),
              isNull(crmApprovalSteps.deletedAt)
            )
          )
      : Promise.resolve([]),
    getWorkflowUsageMap(workflowIds, organizationId)
  ]);

  const stepCountMap = new Map<string, number>();
  for (const step of steps) {
    stepCountMap.set(step.workflowId, (stepCountMap.get(step.workflowId) ?? 0) + 1);
  }

  return rows.map((row) => ({
    ...mapWorkflowRecord(row),
    stepCount: stepCountMap.get(row.id) ?? 0,
    usage: usageMap.get(row.id) ?? {
      totalRequestCount: 0,
      activeRequestCount: 0,
      activeMatrixCount: 0,
      isLocked: false,
      lockedReason: null,
      canEdit: true,
      canDelete: true,
      canDeactivate: true
    }
  }));
}

export async function getApprovalWorkflow(
  id: string,
  organizationId: string
): Promise<ApprovalWorkflowDetail> {
  const workflow = await assertWorkflow(id, organizationId);
  const [steps, usageMap] = await Promise.all([
    listWorkflowStepsInternal(id, organizationId),
    getWorkflowUsageMap([id], organizationId)
  ]);

  return {
    ...mapWorkflowRecord(workflow),
    steps: steps.map(mapStepRecord),
    usage: usageMap.get(id) ?? {
      totalRequestCount: 0,
      activeRequestCount: 0,
      activeMatrixCount: 0,
      isLocked: false,
      lockedReason: null,
      canEdit: true,
      canDelete: true,
      canDeactivate: true
    }
  };
}

export async function createApprovalWorkflow(
  organizationId: string,
  userId: string,
  payload: CreateApprovalWorkflowInput
): Promise<ApprovalWorkflowRecord> {
  await ensureWorkflowCodeUnique(organizationId, payload.code);

  const [created] = await db
    .insert(crmApprovalWorkflows)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      code: payload.code,
      name: payload.name.trim(),
      entityType: payload.entityType.trim(),
      description: payload.description ?? null,
      isSystem: false,
      isActive: payload.isActive ?? true,
      createdBy: userId,
      updatedBy: userId
    })
    .returning();

  return mapWorkflowRecord(created);
}

export async function updateApprovalWorkflow(
  id: string,
  organizationId: string,
  userId: string,
  payload: UpdateApprovalWorkflowInput
): Promise<ApprovalWorkflowRecord> {
  await assertWorkflowEditable(id, organizationId);
  const current = await assertWorkflow(id, organizationId);
  const nextCode = payload.code ?? current.code;

  await ensureWorkflowCodeUnique(organizationId, nextCode, id);

  const [updated] = await db
    .update(crmApprovalWorkflows)
    .set({
      code: nextCode,
      name: payload.name?.trim() ?? current.name,
      entityType: payload.entityType?.trim() ?? current.entityType,
      description: payload.description !== undefined ? payload.description : current.description,
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmApprovalWorkflows.id, id))
    .returning();

  return mapWorkflowRecord(updated);
}

export async function cloneApprovalWorkflow(
  id: string,
  organizationId: string,
  userId: string
): Promise<ApprovalWorkflowDetail> {
  const current = await assertWorkflow(id, organizationId);
  const currentSteps = await listWorkflowStepsInternal(id, organizationId);
  const cloneCode = await nextCloneCode(organizationId, current.code);

  const clonedWorkflow = await db.transaction(async (tx) => {
    const [workflow] = await tx
      .insert(crmApprovalWorkflows)
      .values({
        id: crypto.randomUUID(),
        organizationId,
        code: cloneCode,
        name: `${current.name} Copy`,
        entityType: current.entityType,
        description: current.description,
        isSystem: false,
        isActive: false,
        createdBy: userId,
        updatedBy: userId
      })
      .returning();

    if (currentSteps.length > 0) {
      await tx.insert(crmApprovalSteps).values(
        currentSteps.map((step) => ({
          id: crypto.randomUUID(),
          organizationId,
          workflowId: workflow.id,
          stepNumber: step.stepNumber,
          roleCode: step.roleCode,
          roleName: step.roleName,
          approvalMode: step.approvalMode,
          isRequired: step.isRequired
        }))
      );
    }

    return workflow;
  });

  return getApprovalWorkflow(clonedWorkflow.id, organizationId);
}

export async function activateApprovalWorkflow(
  id: string,
  organizationId: string,
  userId: string
): Promise<ApprovalWorkflowRecord> {
  await assertWorkflowHasRequiredSteps(id, organizationId);
  const [updated] = await db
    .update(crmApprovalWorkflows)
    .set({
      isActive: true,
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmApprovalWorkflows.id, id))
    .returning();

  return mapWorkflowRecord(updated);
}

export async function deactivateApprovalWorkflow(
  id: string,
  organizationId: string,
  userId: string
): Promise<ApprovalWorkflowRecord> {
  await assertWorkflowCanDeactivate(id, organizationId);
  const [updated] = await db
    .update(crmApprovalWorkflows)
    .set({
      isActive: false,
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmApprovalWorkflows.id, id))
    .returning();

  return mapWorkflowRecord(updated);
}

export async function softDeleteApprovalWorkflow(
  id: string,
  organizationId: string,
  userId: string
): Promise<{ before: ApprovalWorkflowDetail; after: ApprovalWorkflowRecord }> {
  const before = await getApprovalWorkflow(id, organizationId);
  await assertWorkflowCanDelete(id, organizationId);
  const [updated] = await db
    .update(crmApprovalWorkflows)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmApprovalWorkflows.id, id))
    .returning();

  return {
    before,
    after: mapWorkflowRecord(updated)
  };
}

export async function listApprovalWorkflowSteps(
  workflowId: string,
  organizationId: string
): Promise<ApprovalStepRecord[]> {
  await assertWorkflow(workflowId, organizationId);
  const steps = await listWorkflowStepsInternal(workflowId, organizationId);
  return steps.map(mapStepRecord);
}

export async function createApprovalWorkflowStep(
  workflowId: string,
  organizationId: string,
  payload: CreateApprovalStepInput
): Promise<ApprovalStepRecord[]> {
  await assertWorkflowEditable(workflowId, organizationId);
  const workflow = await assertWorkflow(workflowId, organizationId);

  await db.transaction(async (tx) => {
    const existingSteps = await tx
      .select()
      .from(crmApprovalSteps)
      .where(
        and(
          eq(crmApprovalSteps.workflowId, workflowId),
          eq(crmApprovalSteps.organizationId, organizationId),
          isNull(crmApprovalSteps.deletedAt)
        )
      )
      .orderBy(asc(crmApprovalSteps.stepNumber));

    const maxStepNumber = existingSteps.length + 1;
    if (payload.stepNumber > maxStepNumber) {
      throw new AuthError(`stepNumber must be between 1 and ${maxStepNumber}`, 400);
    }

    await Promise.all(
      existingSteps
        .filter((step) => step.stepNumber >= payload.stepNumber)
        .sort((left, right) => right.stepNumber - left.stepNumber)
        .map((step) =>
          tx
            .update(crmApprovalSteps)
            .set({
              stepNumber: step.stepNumber + 1,
              updatedAt: new Date()
            })
            .where(eq(crmApprovalSteps.id, step.id))
        )
    );

    await tx.insert(crmApprovalSteps).values({
      id: crypto.randomUUID(),
      organizationId,
      workflowId,
      stepNumber: payload.stepNumber,
      roleCode: payload.roleCode,
      roleName: payload.roleName.trim(),
      approvalMode: payload.approvalMode,
      isRequired: payload.isRequired
    });
  });

  if (workflow.isActive) {
    await assertWorkflowHasRequiredSteps(workflowId, organizationId);
  }

  return listApprovalWorkflowSteps(workflowId, organizationId);
}

export async function updateApprovalWorkflowStep(
  stepId: string,
  organizationId: string,
  payload: UpdateApprovalStepInput
): Promise<ApprovalStepRecord> {
  const current = await assertStep(stepId, organizationId);
  await assertWorkflowEditable(current.workflowId, organizationId);

  const [updated] = await db
    .update(crmApprovalSteps)
    .set({
      roleCode: payload.roleCode ?? current.roleCode,
      roleName: payload.roleName?.trim() ?? current.roleName,
      approvalMode: payload.approvalMode ?? current.approvalMode,
      isRequired: payload.isRequired ?? current.isRequired,
      updatedAt: new Date()
    })
    .where(eq(crmApprovalSteps.id, stepId))
    .returning();

  const workflow = await assertWorkflow(current.workflowId, organizationId);
  if (workflow.isActive) {
    await assertWorkflowHasRequiredSteps(current.workflowId, organizationId);
  }

  return mapStepRecord(updated);
}

export async function deleteApprovalWorkflowStep(
  stepId: string,
  organizationId: string
): Promise<{ before: ApprovalStepRecord; after: ApprovalStepRecord; steps: ApprovalStepRecord[] }> {
  const current = await assertStep(stepId, organizationId);
  await assertWorkflowEditable(current.workflowId, organizationId);
  const workflow = await assertWorkflow(current.workflowId, organizationId);

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

  const remainingSteps = await listWorkflowStepsInternal(current.workflowId, organizationId);
  await Promise.all(
    remainingSteps.map((step, index) =>
      db
        .update(crmApprovalSteps)
        .set({
          stepNumber: index + 1,
          updatedAt: new Date()
        })
        .where(eq(crmApprovalSteps.id, step.id))
    )
  );

  if (workflow.isActive) {
    await assertWorkflowHasRequiredSteps(current.workflowId, organizationId);
  }

  return {
    before: mapStepRecord(current),
    after: mapStepRecord(updated),
    steps: await listApprovalWorkflowSteps(current.workflowId, organizationId)
  };
}

export async function reorderApprovalWorkflowSteps(
  workflowId: string,
  organizationId: string,
  payload: ReorderApprovalStepsInput
): Promise<ApprovalStepRecord[]> {
  await assertWorkflowEditable(workflowId, organizationId);
  const existingSteps = await listWorkflowStepsInternal(workflowId, organizationId);

  if (existingSteps.length !== payload.orderedStepIds.length) {
    throw new AuthError('orderedStepIds must include every active step exactly once', 400);
  }

  const existingIds = new Set(existingSteps.map((step) => step.id));
  for (const stepId of payload.orderedStepIds) {
    if (!existingIds.has(stepId)) {
      throw new AuthError('orderedStepIds contains an unknown step', 400);
    }
  }

  await db.transaction(async (tx) => {
    await Promise.all(
      payload.orderedStepIds.map((stepId, index) =>
        tx
          .update(crmApprovalSteps)
          .set({
            stepNumber: index + 1,
            updatedAt: new Date()
          })
          .where(eq(crmApprovalSteps.id, stepId))
      )
    );
  });

  const workflow = await assertWorkflow(workflowId, organizationId);
  if (workflow.isActive) {
    await assertWorkflowHasRequiredSteps(workflowId, organizationId);
  }

  return listApprovalWorkflowSteps(workflowId, organizationId);
}
