import { clientFetch } from '@/lib/api/client';
import { ApiError } from '@/lib/api/errors';
import { unwrap, type ApiEnvelope, type Paginated, type PageParams } from '@/lib/api/types';
import { ADMIN_QUEUE_PAGE_SIZE } from '../constants';
import type {
  AdminVerificationCase,
  AdminVerificationQueueFilters,
  AdminVerificationQueueItem,
  AdminVerificationStepDetail,
  CredentialDetailsInput,
  DecideStepInput,
  DecideStepResult,
  DocumentConfirmedResult,
  IdentityKycInput,
  NurseCredential,
  RunStepResult,
  SignedDocumentUrl,
  TrustBadge,
  UploadUrlResult,
  VerificationAggregateStatus,
  VerificationApi,
  VerificationDocument,
  VerificationStatus,
  VerificationStepStatus,
} from '../types';

const BASE = '/api/v1/nurse_verification';
const NURSES_BASE = '/api/v1/nurses';
const ADMIN_BASE = '/api/v1/admin_verifications';

/** `AdminPendingStepDto` — one **row per step** awaiting attention (not per nurse); documents carry signed GET URLs. */
interface AdminPendingStepWire {
  nurseVerificationId: number;
  nurseId: number;
  nurseName: string;
  stepId: number;
  stepCode: string;
  stepDisplayName: string;
  status: VerificationStepStatus;
  submittedAt: string | null;
  documents: VerificationDocument[];
}

/** `AdminStepDetailDto` — note the id lives under `stepId` on the wire (mapped to `id`). */
interface AdminStepDetailWire {
  stepId: number;
  code: string;
  displayName: string;
  status: VerificationStepStatus;
  isAutomated: boolean;
  expiresAt: string | null;
  failureReason: string | null;
  documents: VerificationDocument[];
}

/** `AdminVerificationDetailDto`. */
interface AdminVerificationDetailWire {
  nurseVerificationId: number;
  nurseId: number;
  identityName: string;
  status: VerificationAggregateStatus;
  steps: AdminStepDetailWire[];
  credentials: NurseCredential[];
}

/**
 * REQ-034: the queue DTO is **per step**, so we fold rows to one item per nurse for the queue UI. This is
 * lossy — the per-step page carries no whole-nurse aggregate (`stepsPassed`/`stepsTotal`/expiry), and a
 * nurse's steps can straddle page boundaries — which is why a nurse-level queue endpoint is filed. We map
 * what the row gives (nurse identity, the step as `nextPendingStepCode`, `submittedAt`) and leave the
 * unavailable aggregate fields at neutral defaults.
 */
function foldQueueRows(rows: AdminPendingStepWire[]): AdminVerificationQueueItem[] {
  const byNurse = new Map<number, AdminVerificationQueueItem>();
  for (const row of rows) {
    const existing = byNurse.get(row.nurseVerificationId);
    if (existing) {
      if (row.submittedAt && (existing.submittedAt == null || row.submittedAt < existing.submittedAt)) {
        existing.submittedAt = row.submittedAt;
      }
      continue;
    }
    byNurse.set(row.nurseVerificationId, {
      nurseVerificationId: row.nurseVerificationId,
      nurseId: row.nurseId,
      nurseName: row.nurseName,
      status: 'in_review',
      stepsPassed: 0,
      stepsTotal: 0,
      nextPendingStepCode: row.stepCode,
      submittedAt: row.submittedAt,
      hasExpiringCredential: false,
    });
  }
  return Array.from(byNurse.values());
}

function toStepDetail(wire: AdminStepDetailWire): AdminVerificationStepDetail {
  return {
    id: wire.stepId,
    code: wire.code,
    displayName: wire.displayName,
    status: wire.status,
    isAutomated: wire.isAutomated,
    expiresAt: wire.expiresAt,
    failureReason: wire.failureReason,
    documents: wire.documents,
  };
}

/**
 * Computes the browser-side integrity hash the confirm endpoint records against the uploaded bytes
 * (SHA-256 hex). Runs in the browser only (Web Crypto); the mock skips it.
 */
async function sha256Hex(file: File): Promise<string> {
  const buffer = await file.arrayBuffer();
  const digest = await crypto.subtle.digest('SHA-256', buffer);
  return Array.from(new Uint8Array(digest))
    .map((byte) => byte.toString(16).padStart(2, '0'))
    .join('');
}

/**
 * PUTs the file bytes to the signed object-storage URL with upload **progress**. This is a direct PUT
 * to `IObjectStorage` (not our API), so it uses XHR — `fetch` can't report upload progress and the
 * signed URL needs no bearer. The bearer-carrying JSON calls still go through `clientFetch`.
 */
function putSignedUrl(uploadUrl: string, file: File, onProgress?: (percent: number) => void): Promise<void> {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('PUT', uploadUrl);
    xhr.setRequestHeader('Content-Type', file.type);
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) onProgress?.(Math.round((event.loaded / event.total) * 100));
    };
    xhr.onload = () =>
      xhr.status >= 200 && xhr.status < 300
        ? resolve()
        : reject(new ApiError(xhr.status, 'Object storage upload failed', 'upload_failed'));
    xhr.onerror = () => reject(new ApiError(0, 'Network error during upload', 'network_error'));
    xhr.send(file);
  });
}

/**
 * Real HTTP implementation of the VerificationApi seam (b6 contract). Routes are action-style +
 * snake_case; JSON bodies/fields are camelCase; step ids come from the route. Automated-step failures
 * come back as `200` with `stepStatus:"failed"` — surfaced, not thrown. Selected once
 * USE_VERIFICATION_MOCK is false.
 *
 * Gap: `submitCredentialDetails` has no nurse-facing b6 endpoint (admin enters the structured fields on
 * review) — it is filed in `for-backend.md` and no-ops here; the document uploads it accompanies ARE
 * contract-backed (upload_url → PUT → documents). The mock persists the details for the standalone demo.
 */
export const verificationClientApi: VerificationApi = {
  getStatus: async () => unwrap(await clientFetch<ApiEnvelope<VerificationStatus>>(BASE)),

  start: async () =>
    unwrap(await clientFetch<ApiEnvelope<VerificationStatus>>(`${BASE}/submit`, { method: 'POST' })),

  runIdentityKyc: async ({ nationalId, livenessCaptured }: IdentityKycInput) =>
    unwrap(
      await clientFetch<ApiEnvelope<RunStepResult>>(`${BASE}/steps/identity_kyc/run`, {
        method: 'POST',
        body: JSON.stringify({ nationalId, livenessPayload: livenessCaptured ? 'captured' : null }),
      }),
    ),

  runShahkarMatch: async () =>
    unwrap(await clientFetch<ApiEnvelope<RunStepResult>>(`${BASE}/steps/shahkar_match/run`, { method: 'POST' })),

  runBankVerification: async () =>
    unwrap(
      await clientFetch<ApiEnvelope<RunStepResult>>(`${BASE}/steps/bank_account_verification/run`, {
        method: 'POST',
      }),
    ),

  uploadStepDocument: async (stepId, file, onProgress): Promise<VerificationDocument> => {
    const { objectStorageKey, uploadUrl } = unwrap(
      await clientFetch<ApiEnvelope<UploadUrlResult>>(`${BASE}/steps/${stepId}/upload_url`, {
        method: 'POST',
        body: JSON.stringify({ contentType: file.type, fileName: file.name }),
      }),
    );
    await putSignedUrl(uploadUrl, file, onProgress);
    const integrityHash = await sha256Hex(file);
    const confirmed = unwrap(
      await clientFetch<ApiEnvelope<DocumentConfirmedResult>>(`${BASE}/steps/${stepId}/documents`, {
        method: 'POST',
        body: JSON.stringify({
          objectStorageKey,
          integrityHash,
          contentType: file.type,
          fileSizeBytes: file.size,
          originalFileName: file.name,
        }),
      }),
    );
    return {
      id: confirmed.documentId,
      contentType: file.type,
      fileSizeBytes: file.size,
      originalFileName: file.name,
      url: '',
    };
  },

  // No nurse-facing endpoint yet (see gap note above / for-backend.md); the accompanying document
  // uploads carry the real signal. Kept as a seam method so the mock can persist details unchanged.
  submitCredentialDetails: async (_input: CredentialDetailsInput) => {},

  getTrustBadge: async (nurseId) =>
    unwrap(await clientFetch<ApiEnvelope<TrustBadge>>(`${NURSES_BASE}/${nurseId}/trust_badge`)),

  listVerificationQueue: async (
    filters: AdminVerificationQueueFilters,
    params: PageParams,
  ): Promise<Paginated<AdminVerificationQueueItem>> => {
    const query = new URLSearchParams();
    if (filters.status) query.set('status', filters.status);
    query.set('page', String(params.page ?? 1));
    query.set('page_size', String(params.pageSize ?? ADMIN_QUEUE_PAGE_SIZE));
    const page = unwrap(
      await clientFetch<ApiEnvelope<Paginated<AdminPendingStepWire>>>(`${ADMIN_BASE}?${query.toString()}`),
    );
    // REQ-034: `total`/`page`/`pageSize` stay the wire (per-step) values until a nurse-level queue endpoint
    // exists — folding to one item per nurse (see foldQueueRows) makes the count nominal, not exact.
    return { items: foldQueueRows(page.items), total: page.total, page: page.page, pageSize: page.pageSize };
  },

  getVerificationCase: async (nurseVerificationId: number): Promise<AdminVerificationCase> => {
    const wire = unwrap(
      await clientFetch<ApiEnvelope<AdminVerificationDetailWire>>(`${ADMIN_BASE}/${nurseVerificationId}`),
    );
    return {
      nurseVerificationId: wire.nurseVerificationId,
      nurseId: wire.nurseId,
      identityName: wire.identityName,
      status: wire.status,
      steps: wire.steps.map(toStepDetail),
      credentials: wire.credentials,
    };
  },

  // REQ-034: b6 has no per-document signed-URL route (documents already carry a short-lived signed `url` on
  // the case detail). This targets a proposed `GET admin_verifications/documents/{documentId}/url` for an
  // on-demand re-sign; until it ships, callers can re-fetch the case to get a fresh document `url`.
  getDocumentSignedUrl: async (documentId: number): Promise<SignedDocumentUrl> =>
    unwrap(await clientFetch<ApiEnvelope<SignedDocumentUrl>>(`${ADMIN_BASE}/documents/${documentId}/url`)),

  decideStep: async (stepId: number, input: DecideStepInput): Promise<DecideStepResult> =>
    unwrap(
      await clientFetch<ApiEnvelope<DecideStepResult>>(`${ADMIN_BASE}/steps/${stepId}/decide`, {
        method: 'POST',
        body: JSON.stringify(input),
      }),
    ),

  // REQ-034: b6 has no whole-verification approve/reject route — approval emerges from the final step
  // `decide` re-aggregating `is_verified`. These target proposed `POST admin_verifications/{id}/approve` and
  // `/reject` for an explicit admin action (until they ship, approve by deciding the last pending step).
  approveVerification: async (nurseVerificationId: number): Promise<void> => {
    await clientFetch<ApiEnvelope<boolean>>(`${ADMIN_BASE}/${nurseVerificationId}/approve`, { method: 'POST' });
  },

  rejectVerification: async (nurseVerificationId: number, reason: string): Promise<void> => {
    await clientFetch<ApiEnvelope<boolean>>(`${ADMIN_BASE}/${nurseVerificationId}/reject`, {
      method: 'POST',
      body: JSON.stringify({ reason }),
    });
  },
};
