import 'server-only';

import { and, desc, eq, ne } from 'drizzle-orm';
import { setupRuns, setupRunSteps, seedManifests, seedManifestItems } from '@/db/schema';
import { db } from '@/lib/db';

export type SetupRunStatus =
  | 'not_started'
  | 'readiness_checked'
  | 'foundation_installed'
  | 'organization_created'
  | 'administrator_created'
  | 'scope_configured'
  | 'sequences_configured'
  | 'approval_configured'
  | 'csv_validated'
  | 'csv_imported'
  | 'verified'
  | 'completed'
  | 'failed'
  | 'cancelled';

export type SetupStepStatus =
  | 'not_started'
  | 'running'
  | 'passed'
  | 'warning'
  | 'failed'
  | 'skipped'
  | 'stale';

export type SetupRunMode = 'production' | 'demo_uat' | 'dry_run';

export interface SaveSetupStepInput {
  setupRunId?: string;
  stepKey: string;
  status: SetupStepStatus;
  inputHash?: string | null;
  output?: Record<string, unknown> | null;
  errors?: unknown[] | null;
  warnings?: unknown[] | null;
  skipStaleInvalidation?: boolean;
}

const ACTIVE_RUN_STATUSES: SetupRunStatus[] = [
  'not_started',
  'readiness_checked',
  'foundation_installed',
  'organization_created',
  'administrator_created',
  'scope_configured',
  'sequences_configured',
  'approval_configured',
  'csv_validated',
  'csv_imported',
  'verified',
  'failed'
];

const STEP_TO_RUN_STATUS: Record<string, SetupRunStatus> = {
  readiness: 'readiness_checked',
  foundation: 'foundation_installed',
  organization: 'organization_created',
  administrator: 'administrator_created',
  scope: 'scope_configured',
  sequences: 'sequences_configured',
  approval: 'approval_configured',
  csv_preview: 'csv_validated',
  csv_commit: 'csv_imported',
  verification: 'verified'
};

const STALE_DEPENDENCIES: Record<string, string[]> = {
  readiness: ['foundation', 'organization', 'administrator', 'scope', 'sequences', 'approval', 'csv_preview', 'csv_commit', 'verification'],
  organization: ['administrator', 'scope', 'sequences', 'approval', 'csv_preview', 'csv_commit', 'verification'],
  administrator: ['verification'],
  scope: ['sequences', 'approval', 'csv_preview', 'csv_commit', 'verification'],
  sequences: ['verification'],
  approval: ['csv_preview', 'csv_commit', 'verification'],
  csv_preview: ['csv_commit'],
  csv_commit: ['verification'],
  bootstrap_users: [
    'bootstrap_organizations',
    'bootstrap_memberships',
    'bootstrap_crm_role_assignments',
    'bootstrap_master_options',
    'bootstrap_branches',
    'bootstrap_product_types',
    'bootstrap_document_sequences',
    'bootstrap_approval_workflows',
    'bootstrap_approval_steps',
    'bootstrap_approval_matrix'
  ],
  bootstrap_organizations: [
    'bootstrap_memberships',
    'bootstrap_crm_role_assignments',
    'bootstrap_master_options',
    'bootstrap_branches',
    'bootstrap_product_types',
    'bootstrap_document_sequences',
    'bootstrap_approval_workflows',
    'bootstrap_approval_steps',
    'bootstrap_approval_matrix'
  ],
  bootstrap_memberships: [
    'bootstrap_crm_role_assignments',
    'bootstrap_master_options',
    'bootstrap_branches',
    'bootstrap_product_types',
    'bootstrap_document_sequences'
  ],
  bootstrap_master_options: [
    'bootstrap_branches',
    'bootstrap_product_types',
    'bootstrap_document_sequences',
    'bootstrap_approval_matrix'
  ],
  bootstrap_branches: ['bootstrap_document_sequences', 'bootstrap_approval_matrix'],
  bootstrap_product_types: ['bootstrap_document_sequences', 'bootstrap_approval_matrix'],
  bootstrap_approval_workflows: ['bootstrap_approval_steps', 'bootstrap_approval_matrix']
};

function now() {
  return new Date();
}

function createId(prefix: string) {
  return `${prefix}_${crypto.randomUUID()}`;
}

function isActiveRun(status: string): status is SetupRunStatus {
  return ACTIVE_RUN_STATUSES.includes(status as SetupRunStatus);
}

function sanitizeOutput(stepKey: string, output: Record<string, unknown> | null | undefined) {
  if (!output) return null;

  if (stepKey === 'csv_preview') {
    const summary = output.summary ?? null;
    const generatedAt = output.generatedAt ?? null;
    const previewHash = output.previewHash ?? null;
    const files = Array.isArray(output.files)
      ? output.files.map((file) => {
          if (!file || typeof file !== 'object') return file;
          const candidate = file as Record<string, unknown>;
          return {
            fileName: candidate.fileName,
            template: candidate.template,
            importOrder: candidate.importOrder,
            status: candidate.status,
            summary: candidate.summary,
            errors: candidate.errors,
            warnings: candidate.warnings
          };
        })
      : [];

    return { summary, generatedAt, previewHash, files };
  }

  if (stepKey === 'csv_commit') {
    const files = Array.isArray(output.files)
      ? output.files.map((file) => {
          if (!file || typeof file !== 'object') return file;
          const candidate = file as Record<string, unknown>;
          return {
            fileName: candidate.fileName,
            template: candidate.template,
            status: candidate.status,
            imported: candidate.imported,
            updated: candidate.updated,
            skipped: candidate.skipped,
            errors: candidate.errors,
            warnings: candidate.warnings
          };
        })
      : [];

    return {
      status: output.status,
      generatedAt: output.generatedAt,
      previewHash: output.previewHash,
      committedHash: output.committedHash,
      dryRun: output.dryRun,
      summary: output.summary,
      errors: output.errors,
      warnings: output.warnings,
      files
    };
  }

  return output;
}

async function getLatestRunByStatus(statuses: SetupRunStatus[]) {
  const runs = await db.select().from(setupRuns).orderBy(desc(setupRuns.startedAt));
  return runs.find((run) => statuses.includes(run.status as SetupRunStatus)) ?? null;
}

export async function getCurrentSetupRun() {
  const activeRun = await getLatestRunByStatus(ACTIVE_RUN_STATUSES);
  const latestCompletedRun = activeRun ? null : await getLatestRunByStatus(['completed']);
  const run = activeRun ?? latestCompletedRun;

  if (!run) {
    return {
      active: false,
      run: null,
      steps: [],
      manifests: [],
      manifestItems: [],
      completed: false
    };
  }

  const [steps, manifests] = await Promise.all([
    db.select().from(setupRunSteps).where(eq(setupRunSteps.setupRunId, run.id)),
    db.select().from(seedManifests).where(eq(seedManifests.setupRunId, run.id))
  ]);

  const manifestItems =
    manifests.length === 0
      ? []
      : await db.select().from(seedManifestItems).where(
          // D.7.8 keeps this simple: report endpoint can group all items by matching manifest ids in memory.
          ne(seedManifestItems.seedManifestId, '__none__')
        );

  const manifestIds = new Set(manifests.map((manifest) => manifest.id));

  return {
    active: isActiveRun(run.status),
    run,
    steps,
    manifests,
    manifestItems: manifestItems.filter((item) => manifestIds.has(item.seedManifestId)),
    completed: run.status === 'completed'
  };
}

export async function startSetupRun(input: {
  actorId: string;
  mode?: SetupRunMode;
  targetOrganizationId?: string | null;
}) {
  const current = await getCurrentSetupRun();

  if (current.run && current.active) {
    return current;
  }

  const timestamp = now();
  const id = createId('setrun');

  await db.insert(setupRuns).values({
    id,
    status: 'not_started',
    mode: input.mode ?? 'production',
    targetOrganizationId: input.targetOrganizationId ?? null,
    startedBy: input.actorId,
    startedAt: timestamp,
    createdAt: timestamp,
    updatedAt: timestamp
  });

  return getCurrentSetupRun();
}

export async function saveSetupStep(input: SaveSetupStepInput) {
  const current = input.setupRunId
    ? { run: (await db.select().from(setupRuns).where(eq(setupRuns.id, input.setupRunId)))[0] ?? null }
    : await getCurrentSetupRun();

  const run = current.run;
  if (!run) {
    throw new Error('Start a setup run before saving setup steps.');
  }

  const existingStep = (
    await db
      .select()
      .from(setupRunSteps)
      .where(and(eq(setupRunSteps.setupRunId, run.id), eq(setupRunSteps.stepKey, input.stepKey)))
  )[0];
  const timestamp = now();
  const sanitizedOutput = sanitizeOutput(input.stepKey, input.output);
  const completedAt = input.status === 'running' || input.status === 'not_started' ? null : timestamp;
  const inputChanged =
    Boolean(existingStep) &&
    existingStep.inputHash !== null &&
    input.inputHash !== null &&
    existingStep.inputHash !== input.inputHash;

  await db
    .insert(setupRunSteps)
    .values({
      id: existingStep?.id ?? createId('setstep'),
      setupRunId: run.id,
      stepKey: input.stepKey,
      status: input.status,
      inputHash: input.inputHash ?? null,
      outputJson: sanitizedOutput,
      errorsJson: input.errors ?? null,
      warningsJson: input.warnings ?? null,
      startedAt: existingStep?.startedAt ?? timestamp,
      completedAt,
      createdAt: existingStep?.createdAt ?? timestamp,
      updatedAt: timestamp
    })
    .onConflictDoUpdate({
      target: [setupRunSteps.setupRunId, setupRunSteps.stepKey],
      set: {
        status: input.status,
        inputHash: input.inputHash ?? null,
        outputJson: sanitizedOutput,
        errorsJson: input.errors ?? null,
        warningsJson: input.warnings ?? null,
        completedAt,
        updatedAt: timestamp
      }
    });

  if (inputChanged && !input.skipStaleInvalidation) {
    const staleSteps = STALE_DEPENDENCIES[input.stepKey] ?? [];
    await Promise.all(
      staleSteps.map((stepKey) =>
        db
          .update(setupRunSteps)
          .set({ status: 'stale', updatedAt: timestamp })
          .where(and(eq(setupRunSteps.setupRunId, run.id), eq(setupRunSteps.stepKey, stepKey)))
      )
    );
  }

  const nextRunStatus =
    input.status === 'failed' ? 'failed' : STEP_TO_RUN_STATUS[input.stepKey] ?? (run.status as SetupRunStatus);

  await db.update(setupRuns).set({ status: nextRunStatus, updatedAt: timestamp }).where(eq(setupRuns.id, run.id));

  return getCurrentSetupRun();
}

export async function completeSetupRun(input: { actorId: string; setupRunId?: string; report?: Record<string, unknown> }) {
  const current = input.setupRunId
    ? { run: (await db.select().from(setupRuns).where(eq(setupRuns.id, input.setupRunId)))[0] ?? null }
    : await getCurrentSetupRun();

  const run = current.run;
  if (!run) {
    throw new Error('Start a setup run before completing setup.');
  }

  const timestamp = now();
  await db
    .update(setupRuns)
    .set({
      status: 'completed',
      completedBy: input.actorId,
      completedAt: timestamp,
      metadata: input.report ?? null,
      updatedAt: timestamp
    })
    .where(eq(setupRuns.id, run.id));

  return getCurrentSetupRun();
}

export async function getSetupRunReport() {
  return getCurrentSetupRun();
}

export async function getActiveSetupRunId() {
  const current = await getCurrentSetupRun();
  return current.active ? current.run?.id ?? null : null;
}
