import { sleep } from '@/utils';
import { ApiError } from '@/lib/api/errors';
import type {
  AdminVerificationCase,
  AdminVerificationQueueItem,
  AdminVerificationStepDetail,
  CredentialType,
  IdentityKycInput,
  NurseCredential,
  StepTypeCode,
  TrustBadge,
  VerificationApi,
  VerificationDocument,
  VerificationStatus,
  VerificationStep,
  VerificationStepStatus,
} from '../types';
import { ADMIN_QUEUE_PAGE_SIZE, NATIONAL_ID_LENGTH } from '../constants';

const MOCK_LATENCY_MS = 300;

/**
 * The seeded step-types, in checklist order (mirrors the b6 required step-type seed). Every step is
 * required, so `steps.length` is the "Y" of the "X از Y" meter. `automated` drives the honest copy and
 * whether the step runs (`/run`) or waits on a manual document + admin decision.
 */
const SEED: ReadonlyArray<{ code: StepTypeCode; automated: boolean }> = [
  { code: 'identity_kyc', automated: true },
  { code: 'shahkar_match', automated: true },
  { code: 'moh_competency_license', automated: false },
  { code: 'ino_membership', automated: false },
  { code: 'criminal_record', automated: false },
  { code: 'bank_account_verification', automated: true },
];

// Deterministic test triggers matching the backend b6 mock seams (documented in the mock registry).
const KYC_FAIL_NATIONAL_ID = '0000000000'; // MockIdentityKycProvider fail id
const SHAHKAR_SHARED_SIM_NATIONAL_ID = '1111111111'; // stands in for the shared-SIM handled failure

let steps: VerificationStep[] = [];
let nextStepId = 1;
let nextDocId = 1;
let boundNationalId = '';
let approvedAt: string | null = null;

const nationalIdShape = new RegExp(`^\\d{${NATIONAL_ID_LENGTH}}$`);

function findStep(code: string): VerificationStep | undefined {
  return steps.find((step) => step.code === code);
}

function setStepStatus(code: string, status: VerificationStepStatus, failureReason: string | null = null): void {
  steps = steps.map((step) => (step.code === code ? { ...step, status, failureReason } : step));
}

/** Re-aggregate exactly as the server would: approved only when every step passes; blockers are the rest. */
function aggregate(): VerificationStatus {
  if (steps.length === 0) {
    return { status: 'not_started', isBookable: false, blockingSteps: [], steps: [] };
  }
  const blockingSteps = steps.filter((step) => step.status !== 'passed').map((step) => step.code);
  const allPassed = blockingSteps.length === 0;
  const anyInReview = steps.some((step) => step.status === 'in_review');
  const status = allPassed ? 'approved' : anyInReview ? 'in_review' : 'pending';
  return { status, isBookable: allPassed, blockingSteps, steps: steps.map((step) => ({ ...step })) };
}

function seedSteps(): void {
  if (steps.length > 0) return; // idempotent — never duplicates a step (contract submit semantics)
  steps = SEED.map(({ code, automated }) => ({
    id: nextStepId++,
    code,
    displayName: code,
    status: 'not_started',
    isAutomated: automated,
    expiresAt: null,
    failureReason: null,
  }));
}

/* --- Admin review queue fixtures + state ---------------------------------------------------------
 * A small in-memory review desk: three nurses spanning `pending` / `in_review`, each a full case with
 * ordered steps (automated steps `passed`, a manual credential-bearing step `in_review` with a document),
 * one nurse carrying an expiring credential. Decisions mutate this state so a human can watch a case move
 * through decide → approve/reject and drop off the queue. Timestamps are relative to module-load `Date.now()`.
 */

const CREDENTIAL_BEARING: ReadonlySet<string> = new Set([
  'moh_competency_license',
  'ino_membership',
  'criminal_record',
]);

/** Internal record: the admin case + the queue-only metadata (`nurseName`, `submittedAt`, expiry flag). */
interface AdminCaseRecord extends AdminVerificationCase {
  nurseName: string;
  submittedAt: string | null;
  hasExpiringCredential: boolean;
}

let nextAdminStepId = 1;
let nextCredentialId = 8001;

const adminNowMs = Date.now();
const daysAgo = (n: number): string => new Date(adminNowMs - n * 24 * 60 * 60 * 1000).toISOString();
const daysFromNow = (n: number): string => new Date(adminNowMs + n * 24 * 60 * 60 * 1000).toISOString();

function mkStep(
  code: StepTypeCode,
  status: VerificationStepStatus,
  isAutomated: boolean,
  extra: Partial<Pick<AdminVerificationStepDetail, 'expiresAt' | 'failureReason' | 'documents'>> = {},
): AdminVerificationStepDetail {
  return {
    id: nextAdminStepId++,
    code,
    displayName: code,
    status,
    isAutomated,
    expiresAt: extra.expiresAt ?? null,
    failureReason: extra.failureReason ?? null,
    documents: extra.documents ?? [],
  };
}

function mkDoc(id: number, originalFileName: string): VerificationDocument {
  return {
    id,
    contentType: 'application/pdf',
    fileSizeBytes: 482_000,
    originalFileName,
    // A short-lived signed GET URL; the on-demand `getDocumentSignedUrl` re-signs it fresh each open.
    url: `https://mock.balinyaar.local/docs/${id}`,
  };
}

function mkCredential(
  credentialType: CredentialType,
  holderNameSnapshot: string,
  issuingAuthority: string,
  opts: { issuedAt?: string | null; expiresAt?: string | null } = {},
): NurseCredential {
  return {
    id: nextCredentialId++,
    credentialType,
    holderNameSnapshot,
    issuingAuthority,
    issuedAt: opts.issuedAt ?? null,
    expiresAt: opts.expiresAt ?? null,
    verificationMethod: 'manual',
  };
}

const adminCases: AdminCaseRecord[] = [
  {
    nurseVerificationId: 501,
    nurseId: 101,
    identityName: 'مریم رضایی',
    nurseName: 'مریم رضایی',
    status: 'in_review',
    submittedAt: daysAgo(2),
    hasExpiringCredential: false,
    steps: [
      mkStep('identity_kyc', 'passed', true),
      mkStep('shahkar_match', 'passed', true),
      mkStep('moh_competency_license', 'in_review', false, { documents: [mkDoc(9001, 'moh-license.pdf')] }),
      mkStep('ino_membership', 'pending', false),
      mkStep('criminal_record', 'pending', false),
      mkStep('bank_account_verification', 'passed', true),
    ],
    credentials: [],
  },
  {
    nurseVerificationId: 502,
    nurseId: 102,
    identityName: 'زهرا محمدی',
    nurseName: 'زهرا محمدی',
    status: 'pending',
    submittedAt: daysAgo(5),
    hasExpiringCredential: true,
    steps: [
      mkStep('identity_kyc', 'passed', true),
      mkStep('shahkar_match', 'passed', true),
      mkStep('moh_competency_license', 'pending', false),
      mkStep('ino_membership', 'pending', false),
      mkStep('criminal_record', 'passed', false, { expiresAt: daysFromNow(18) }),
      mkStep('bank_account_verification', 'passed', true),
    ],
    // A recorded criminal-record credential lapsing soon — drives the `hasExpiringCredential` queue chip.
    credentials: [mkCredential('criminal_record', 'زهرا محمدی', 'ناجا', { issuedAt: daysAgo(347), expiresAt: daysFromNow(18) })],
  },
  {
    nurseVerificationId: 503,
    nurseId: 103,
    identityName: 'علی کریمی',
    nurseName: 'علی کریمی',
    status: 'in_review',
    submittedAt: daysAgo(1),
    hasExpiringCredential: false,
    steps: [
      mkStep('identity_kyc', 'passed', true),
      mkStep('shahkar_match', 'passed', true),
      mkStep('moh_competency_license', 'passed', false),
      mkStep('ino_membership', 'in_review', false, { documents: [mkDoc(9002, 'ino-membership.pdf')] }),
      mkStep('criminal_record', 'passed', false, { expiresAt: daysFromNow(300) }),
      mkStep('bank_account_verification', 'passed', true),
    ],
    credentials: [
      mkCredential('moh_competency_license', 'علی کریمی', 'وزارت بهداشت', { issuedAt: daysAgo(120) }),
      mkCredential('criminal_record', 'علی کریمی', 'ناجا', { issuedAt: daysAgo(65), expiresAt: daysFromNow(300) }),
    ],
  },
];

function findCaseById(nurseVerificationId: number): AdminCaseRecord | undefined {
  return adminCases.find((record) => record.nurseVerificationId === nurseVerificationId);
}

function findCaseByStepId(stepId: number): { record: AdminCaseRecord; step: AdminVerificationStepDetail } | undefined {
  for (const record of adminCases) {
    const step = record.steps.find((candidate) => candidate.id === stepId);
    if (step) return { record, step };
  }
  return undefined;
}

/** The next step needing an admin's eyes: an `in_review` (uploaded, awaiting decision) step first, else a `pending` one. */
function nextPendingCode(steps: AdminVerificationStepDetail[]): string | null {
  return (
    steps.find((step) => step.status === 'in_review')?.code ??
    steps.find((step) => step.status === 'pending')?.code ??
    null
  );
}

/** Re-aggregate the case status exactly as the server would after a step decision. */
function reaggregateCase(record: AdminCaseRecord): void {
  const allPassed = record.steps.every((step) => step.status === 'passed');
  const anyInReview = record.steps.some((step) => step.status === 'in_review');
  record.status = allPassed ? 'approved' : anyInReview ? 'in_review' : 'pending';
}

function toQueueItem(record: AdminCaseRecord): AdminVerificationQueueItem {
  return {
    nurseVerificationId: record.nurseVerificationId,
    nurseId: record.nurseId,
    nurseName: record.nurseName,
    status: record.status,
    stepsPassed: record.steps.filter((step) => step.status === 'passed').length,
    stepsTotal: record.steps.length,
    nextPendingStepCode: nextPendingCode(record.steps),
    submittedAt: record.submittedAt,
    hasExpiringCredential: record.hasExpiringCredential,
  };
}

/** Return the admin-case view (drops the queue-only metadata; deep-copies so callers can't mutate the store). */
function toCaseView(record: AdminCaseRecord): AdminVerificationCase {
  return {
    nurseVerificationId: record.nurseVerificationId,
    nurseId: record.nurseId,
    identityName: record.identityName,
    status: record.status,
    steps: record.steps.map((step) => ({ ...step, documents: step.documents.map((doc) => ({ ...doc })) })),
    credentials: record.credentials.map((credential) => ({ ...credential })),
  };
}

/**
 * In-memory mock behind the VerificationApi seam. Drives the whole nurse journey end-to-end — the
 * automated runs (identity/shahkar/bank), the manual document uploads (→ in_review), the structured
 * credential details, and the admin-approval simulation (`__mockApproveAll`) that flips the aggregate
 * to `approved` so a human can watch the trust badge + publish gate unlock. Mirrors the real shapes for
 * a one-line swap.
 */
export const verificationMockApi: VerificationApi = {
  getStatus: async () => {
    await sleep(MOCK_LATENCY_MS);
    return aggregate();
  },

  start: async () => {
    await sleep(MOCK_LATENCY_MS);
    seedSteps();
    return aggregate();
  },

  runIdentityKyc: async ({ nationalId }: IdentityKycInput) => {
    await sleep(MOCK_LATENCY_MS);
    if (!nationalIdShape.test(nationalId)) {
      throw new ApiError(400, 'Malformed national id', 'invalid_national_id');
    }
    boundNationalId = nationalId;
    const step = findStep('identity_kyc');
    if (!step) throw new ApiError(400, 'Verification not started', 'not_started');
    if (nationalId === KYC_FAIL_NATIONAL_ID) {
      setStepStatus('identity_kyc', 'failed', 'kyc_no_match');
      return { stepId: step.id, stepStatus: 'failed', failureReason: 'kyc_no_match' };
    }
    setStepStatus('identity_kyc', 'passed');
    return { stepId: step.id, stepStatus: 'passed', failureReason: null };
  },

  runShahkarMatch: async () => {
    await sleep(MOCK_LATENCY_MS);
    const step = findStep('shahkar_match');
    if (!step) throw new ApiError(400, 'Verification not started', 'not_started');
    if (findStep('identity_kyc')?.status !== 'passed') {
      throw new ApiError(400, 'Identity KYC required first', 'kyc_required');
    }
    if (boundNationalId === SHAHKAR_SHARED_SIM_NATIONAL_ID) {
      setStepStatus('shahkar_match', 'failed', 'shared_sim');
      return { stepId: step.id, stepStatus: 'failed', failureReason: 'shared_sim' };
    }
    setStepStatus('shahkar_match', 'passed');
    return { stepId: step.id, stepStatus: 'passed', failureReason: null };
  },

  runBankVerification: async () => {
    await sleep(MOCK_LATENCY_MS);
    const step = findStep('bank_account_verification');
    if (!step) throw new ApiError(400, 'Verification not started', 'not_started');
    if (findStep('identity_kyc')?.status !== 'passed') {
      throw new ApiError(400, 'Identity KYC required first', 'kyc_required');
    }
    setStepStatus('bank_account_verification', 'passed');
    return { stepId: step.id, stepStatus: 'passed', failureReason: null };
  },

  uploadStepDocument: async (stepId, file, onProgress) => {
    // Simulate the signed-URL PUT progress, then confirm (→ in_review).
    for (let percent = 0; percent <= 100; percent += 25) {
      onProgress?.(percent);
      await sleep(MOCK_LATENCY_MS / 5);
    }
    const step = steps.find((candidate) => candidate.id === stepId);
    if (!step) throw new ApiError(404, 'Step not found', 'not_found');
    setStepStatus(step.code, 'in_review');
    return {
      id: nextDocId++,
      contentType: file.type,
      fileSizeBytes: file.size,
      originalFileName: file.name,
      url: '',
    } satisfies VerificationDocument;
  },

  submitCredentialDetails: async (input) => {
    await sleep(MOCK_LATENCY_MS);
    // The server validates the structured registry fields; the mock enforces the one the UI collects.
    if (input.inoNumber.trim().length === 0) {
      throw new ApiError(400, 'INO number is required', 'ino_number_required');
    }
  },

  getTrustBadge: async (nurseId) => {
    await sleep(MOCK_LATENCY_MS);
    const agg = aggregate();
    return {
      nurseId,
      isVerified: agg.status === 'approved' && agg.isBookable,
      approvedAt,
      credentialTypes: agg.status === 'approved' ? ['moh_competency_license', 'ino_membership'] : [],
    } satisfies TrustBadge;
  },

  listVerificationQueue: async (filters, params) => {
    await sleep(MOCK_LATENCY_MS);
    // Default (no status filter) shows the whole desk — both `pending` and `in_review`.
    const wanted: ReadonlyArray<AdminCaseRecord['status']> = filters.status ? [filters.status] : ['pending', 'in_review'];
    const matched = adminCases.filter((record) => wanted.includes(record.status)).map(toQueueItem);
    const page = params.page ?? 1;
    const pageSize = params.pageSize ?? ADMIN_QUEUE_PAGE_SIZE;
    const start = (page - 1) * pageSize;
    return { items: matched.slice(start, start + pageSize), total: matched.length, page, pageSize };
  },

  getVerificationCase: async (nurseVerificationId) => {
    await sleep(MOCK_LATENCY_MS);
    const record = findCaseById(nurseVerificationId);
    if (!record) throw new ApiError(404, 'Verification not found', 'not_found');
    return toCaseView(record);
  },

  getDocumentSignedUrl: async (documentId) => {
    await sleep(MOCK_LATENCY_MS);
    // Sentinel for the viewer's error/re-request path: this document can never be signed.
    if (documentId === 9999) {
      throw new ApiError(404, 'Document not found', 'document_not_found');
    }
    // A FRESH short-lived URL each call — the signature + timestamp differ so it is never re-used from cache.
    const sig = Math.random().toString(36).slice(2, 12);
    return {
      url: `https://mock.balinyaar.local/docs/${documentId}?sig=${sig}&t=${Date.now()}`,
      expiresInSeconds: 60,
    };
  },

  decideStep: async (stepId, input) => {
    await sleep(MOCK_LATENCY_MS);
    const found = findCaseByStepId(stepId);
    if (!found) throw new ApiError(404, 'Step not found', 'not_found');
    const { record, step } = found;
    if (input.approve) {
      step.status = 'passed';
      step.failureReason = null;
      let credentialId: number | null = null;
      // On approving a credential-bearing step with a credential number, record the (encrypted-at-rest,
      // never re-serialized) credential — mirroring the server's `nurse_credentials` write.
      if (CREDENTIAL_BEARING.has(step.code) && input.credentialNumber) {
        const credential = mkCredential(
          step.code as CredentialType,
          input.holderName ?? record.identityName,
          input.issuingAuthority ?? '',
          { issuedAt: input.issuedAt ?? null, expiresAt: input.expiresAt ?? null },
        );
        record.credentials.push(credential);
        credentialId = credential.id;
      }
      reaggregateCase(record);
      return { stepId, stepStatus: step.status, credentialId };
    }
    const reason = input.rejectionReason?.trim();
    if (!reason) throw new ApiError(400, 'Rejection reason is required', 'rejection_reason_required');
    step.status = 'failed';
    step.failureReason = reason;
    reaggregateCase(record);
    return { stepId, stepStatus: step.status, credentialId: null };
  },

  approveVerification: async (nurseVerificationId) => {
    await sleep(MOCK_LATENCY_MS);
    const record = findCaseById(nurseVerificationId);
    if (!record) throw new ApiError(404, 'Verification not found', 'not_found');
    record.steps = record.steps.map((step) => ({ ...step, status: 'passed', failureReason: null }));
    // Aggregate → `approved`, which drops it out of the queue's `pending`/`in_review` filter.
    record.status = 'approved';
  },

  rejectVerification: async (nurseVerificationId, reason) => {
    await sleep(MOCK_LATENCY_MS);
    const record = findCaseById(nurseVerificationId);
    if (!record) throw new ApiError(404, 'Verification not found', 'not_found');
    if (reason.trim().length === 0) throw new ApiError(400, 'Rejection reason is required', 'rejection_reason_required');
    // Aggregate → `rejected`, dropping it out of the queue.
    record.status = 'rejected';
  },
};

/**
 * Dev-only admin-decision simulation (reachable from B3/B6 while `USE_VERIFICATION_MOCK` is true) — the
 * b6 admin review queue is deferred to f15, so this stands in to let a human **observe** the state
 * change: passes every step and flips the aggregate to `approved`. Never shipped against the real
 * backend (the caller gates it on the mock flag).
 */
export function __mockApproveAll(): void {
  approvedAt = '2026-07-09T00:00:00.000Z';
  steps = steps.map((step) => ({ ...step, status: 'passed', failureReason: null }));
}

/** Dev-only: reject a manual step with a reason, to exercise the rejected-with-reason re-submit path. */
export function __mockRejectStep(code: string, reason: string): void {
  setStepStatus(code, 'failed', reason);
}
