import { sleep } from '@/utils';
import { ApiError } from '@/lib/api/errors';
import type { Paginated } from '@/lib/api/types';
import {
  BOOKING_REQUEST_PAGE_SIZE,
  CUSTOMER_NOTES_MAX_LENGTH,
  MOCK_PAYMENT_DEADLINE_MINUTES,
  MOCK_RESPONSE_DEADLINE_MINUTES,
  REJECTION_REASON_MAX_LENGTH,
} from '../constants';
import type {
  BookingRequestDisplayContext,
  BookingRequestDto,
  BookingRequestListItem,
  BookingRequestListParams,
  BookingRequestsApi,
  BookingRequestStatus,
  CreateBookingRequestPayload,
  RejectBookingRequestPayload,
  RequestRole,
} from '../types';
import { isTerminalBookingRequestStatus } from '../types';

const MOCK_LATENCY_MS = 350;

// Shared, module-level store so the customer's created request appears in the nurse inbox and a nurse
// accept/reject flips the customer's polled C5 — all within one browser session (one module instance).
// Seeded with two pending requests so a nurse-only visit sees a non-empty inbox before any create.
let store: BookingRequestDto[] = [];
let nextId = 1;

const minutesFromNow = (minutes: number) => new Date(Date.now() + minutes * 60_000).toISOString();

function seedRow(overrides: Partial<BookingRequestDto>): BookingRequestDto {
  const id = nextId++;
  return {
    id,
    status: 'pending_nurse_response',
    bookingId: null,
    nurseId: 1,
    nurseName: 'مریم رضایی',
    nurseRating: 4.9,
    nurseTotalReviews: 37,
    patientId: 900 + id,
    patientName: 'حاج‌آقا موسوی',
    variantId: 11,
    variantLabel: 'مراقبت سالمند — شیفت روز',
    variantPriceUnit: 'per_hour',
    variantPrice: '2800000',
    customerAddressId: 800 + id,
    addressTitle: 'منزل',
    cityId: 101,
    cityNameFa: 'تهران',
    cityNameEn: 'Tehran',
    districtId: 1003,
    districtNameFa: 'سعادت‌آباد',
    districtNameEn: 'Saadat Abad',
    addressLine: 'خیابان نمونه، کوچه دوم، پلاک ۱۲',
    postalCode: '1998887766',
    recipientName: 'زهرا موسوی',
    recipientPhone: '09121234567',
    requiredCaregiverGender: 'female',
    requestedDate: new Date(Date.now() + 2 * 86_400_000).toISOString().slice(0, 10),
    requestedTimeStart: '09:00:00',
    requestedTimeEnd: '13:00:00',
    customerNotes: 'لطفاً در تعویض سِرُم دقت شود.',
    nurseResponseDeadlineAt: minutesFromNow(MOCK_RESPONSE_DEADLINE_MINUTES),
    paymentDeadlineAt: null,
    nurseRejectionReason: null,
    createdAt: new Date().toISOString(),
    ...overrides,
  };
}

store = [
  seedRow({}),
  seedRow({
    nurseName: 'مریم رضایی',
    patientName: 'خانم احمدی',
    variantLabel: 'مراقبت پس از جراحی',
    requiredCaregiverGender: 'female',
    customerNotes: 'بیمار پس از عمل زانو، نیاز به کمک در جابه‌جایی دارد.',
    requestedTimeStart: '15:00:00',
    requestedTimeEnd: '19:00:00',
  }),
];

/**
 * Lazily transition any past-deadline row to its terminal state — the client stand-in for the server's
 * background expiry sweep, so `expired_no_response` / `payment_deadline_expired` surface through the poll
 * without a real cron. Runs on every read/write.
 */
function sweep(): void {
  const now = Date.now();
  store = store.map((row) => {
    if (row.status === 'pending_nurse_response' && Date.parse(row.nurseResponseDeadlineAt) < now) {
      return { ...row, status: 'expired_no_response' as BookingRequestStatus };
    }
    if (
      row.status === 'accepted_awaiting_payment' &&
      row.paymentDeadlineAt != null &&
      Date.parse(row.paymentDeadlineAt) < now
    ) {
      return { ...row, status: 'payment_deadline_expired' as BookingRequestStatus };
    }
    return row;
  });
}

function find(id: number): BookingRequestDto {
  const row = store.find((r) => r.id === id);
  // Party-scoping is not modelled in the single-session mock; existence is not leaked either way (404).
  if (!row) throw new ApiError(404, 'Booking request not found', 'not_found');
  return row;
}

/** The nurse view masks the full address (two-stage disclosure) — coarse city/district only. */
function maskForNurse(dto: BookingRequestDto): BookingRequestDto {
  return { ...dto, addressLine: null, postalCode: null, recipientName: null, recipientPhone: null };
}

function toListItem(dto: BookingRequestDto, role: RequestRole): BookingRequestListItem {
  return {
    id: dto.id,
    status: dto.status,
    counterpartyName: role === 'nurse' ? dto.patientName : dto.nurseName,
    nurseRating: role === 'customer' ? dto.nurseRating : null,
    requiredCaregiverGender: dto.requiredCaregiverGender,
    requestedDate: dto.requestedDate,
    requestedTimeStart: dto.requestedTimeStart,
    requestedTimeEnd: dto.requestedTimeEnd,
    nurseResponseDeadlineAt: dto.nurseResponseDeadlineAt,
    paymentDeadlineAt: dto.paymentDeadlineAt,
    customerNotes: role === 'nurse' ? dto.customerNotes : null,
  };
}

// Actionable (non-terminal) rows first, then by soonest response deadline — mirrors the contract's
// "actionable rows sort first".
function actionableFirst(a: BookingRequestDto, b: BookingRequestDto): number {
  const at = isTerminalBookingRequestStatus(a.status) ? 1 : 0;
  const bt = isTerminalBookingRequestStatus(b.status) ? 1 : 0;
  if (at !== bt) return at - bt;
  return Date.parse(a.nurseResponseDeadlineAt) - Date.parse(b.nurseResponseDeadlineAt);
}

function assertCreateValid(payload: CreateBookingRequestPayload): void {
  if (!payload.requiredCaregiverGender) {
    throw new ApiError(400, 'requiredCaregiverGender is required', 'gender_required');
  }
  if (payload.requestedTimeEnd <= payload.requestedTimeStart) {
    throw new ApiError(400, 'requestedTimeEnd must be after requestedTimeStart', 'invalid_time_range');
  }
  if ((payload.customerNotes?.length ?? 0) > CUSTOMER_NOTES_MAX_LENGTH) {
    throw new ApiError(400, 'customerNotes too long', 'notes_too_long');
  }
  const start = Date.parse(`${payload.requestedDate}T${payload.requestedTimeStart}`);
  if (Number.isFinite(start) && start < Date.now()) {
    throw new ApiError(400, 'requestedDate/time is in the past', 'past_date');
  }
}

function buildFromContext(
  id: number,
  payload: CreateBookingRequestPayload,
  context: BookingRequestDisplayContext | undefined,
): BookingRequestDto {
  return {
    id,
    status: 'pending_nurse_response',
    bookingId: null,
    nurseId: payload.nurseId,
    nurseName: context?.nurseName ?? '',
    nurseRating: context?.nurseRating ?? 0,
    nurseTotalReviews: context?.nurseTotalReviews ?? 0,
    patientId: payload.patientId,
    patientName: context?.patientName ?? '',
    variantId: payload.variantId,
    variantLabel: context?.variantLabel ?? '',
    variantPriceUnit: context?.variantPriceUnit ?? 'per_hour',
    variantPrice: context?.variantPrice ?? null,
    customerAddressId: payload.customerAddressId,
    addressTitle: context?.addressTitle ?? '',
    cityId: context?.cityId ?? 0,
    cityNameFa: context?.cityNameFa ?? '',
    cityNameEn: context?.cityNameEn ?? '',
    districtId: context?.districtId ?? null,
    districtNameFa: context?.districtNameFa ?? null,
    districtNameEn: context?.districtNameEn ?? null,
    addressLine: context?.addressLine ?? null,
    postalCode: context?.postalCode ?? null,
    recipientName: context?.recipientName ?? null,
    recipientPhone: context?.recipientPhone ?? null,
    requiredCaregiverGender: payload.requiredCaregiverGender,
    requestedDate: payload.requestedDate,
    requestedTimeStart: payload.requestedTimeStart,
    requestedTimeEnd: payload.requestedTimeEnd,
    customerNotes: payload.customerNotes?.trim() || null,
    nurseResponseDeadlineAt: minutesFromNow(MOCK_RESPONSE_DEADLINE_MINUTES),
    paymentDeadlineAt: null,
    nurseRejectionReason: null,
    createdAt: new Date().toISOString(),
  };
}

/**
 * In-memory mock behind the `BookingRequestsApi` seam. Drives the full request lifecycle over shared
 * session state so C4 → C5 → the nurse inbox → accept/reject/expire all demo end-to-end without a live
 * b8 backend (and without the upstream mock domains' ids needing to exist server-side). Mirrors the real
 * shapes + status/deadline/masking semantics for a one-line swap once the stack is live
 * (`USE_BOOKING_REQUESTS_MOCK = false`).
 */
export const bookingRequestsMockApi: BookingRequestsApi = {
  create: async (payload, context) => {
    await sleep(MOCK_LATENCY_MS);
    assertCreateValid(payload);
    const dto = buildFromContext(nextId++, payload, context);
    store = [dto, ...store];
    return dto;
  },

  get: async (id, role) => {
    await sleep(MOCK_LATENCY_MS);
    sweep();
    const dto = find(id);
    return role === 'nurse' ? maskForNurse(dto) : dto;
  },

  list: async (params: BookingRequestListParams): Promise<Paginated<BookingRequestListItem>> => {
    await sleep(MOCK_LATENCY_MS);
    sweep();
    const matched = [...store]
      .filter((row) => (params.status ? row.status === params.status : true))
      .sort(actionableFirst)
      .map((row) => toListItem(row, params.role));

    const page = params.page ?? 1;
    const pageSize = params.pageSize ?? BOOKING_REQUEST_PAGE_SIZE;
    const start = (page - 1) * pageSize;
    return { items: matched.slice(start, start + pageSize), total: matched.length, page, pageSize };
  },

  accept: async (id) => {
    await sleep(MOCK_LATENCY_MS);
    sweep();
    const dto = find(id);
    if (dto.status !== 'pending_nurse_response') {
      throw new ApiError(409, 'Request is not pending', 'not_pending');
    }
    const updated: BookingRequestDto = {
      ...dto,
      status: 'accepted_awaiting_payment',
      paymentDeadlineAt: minutesFromNow(MOCK_PAYMENT_DEADLINE_MINUTES),
    };
    store = store.map((row) => (row.id === id ? updated : row));
    return maskForNurse(updated);
  },

  reject: async (id, payload: RejectBookingRequestPayload) => {
    await sleep(MOCK_LATENCY_MS);
    sweep();
    const dto = find(id);
    const reason = payload.reason?.trim() ?? '';
    if (!reason) throw new ApiError(400, 'reason is required', 'reason_required');
    if (reason.length > REJECTION_REASON_MAX_LENGTH) {
      throw new ApiError(400, 'reason too long', 'reason_too_long');
    }
    if (dto.status !== 'pending_nurse_response') {
      throw new ApiError(409, 'Request is not pending', 'not_pending');
    }
    const updated: BookingRequestDto = { ...dto, status: 'rejected_by_nurse', nurseRejectionReason: reason };
    store = store.map((row) => (row.id === id ? updated : row));
    return maskForNurse(updated);
  },

  cancel: async (id) => {
    await sleep(MOCK_LATENCY_MS);
    sweep();
    const dto = find(id);
    if (dto.status !== 'pending_nurse_response' && dto.status !== 'accepted_awaiting_payment') {
      throw new ApiError(409, 'Request can no longer be cancelled', 'not_cancellable');
    }
    const updated: BookingRequestDto = { ...dto, status: 'cancelled_by_customer' };
    store = store.map((row) => (row.id === id ? updated : row));
    return updated;
  },
};

/**
 * Mock-only capture bridge (f9): the payment mock's stand-in for the server's webhook-confirm →
 * `BookingFactory` step. Marks a paid request `converted` and stamps the client-augmented `bookingId`
 * (REQ-017) so C5's terminal card and the checkout confirmation can deep-link the booking. NOT part of
 * the `BookingRequestsApi` seam — no real endpoint does this from the client; only `services/payment`'s
 * mock imports it.
 */
export function mockMarkBookingRequestConverted(id: number, bookingId: number): BookingRequestDto {
  const dto = find(id);
  if (dto.status !== 'accepted_awaiting_payment') {
    throw new ApiError(409, 'Request is not awaiting payment', 'not_awaiting_payment');
  }
  const updated: BookingRequestDto = { ...dto, status: 'converted', bookingId };
  store = store.map((row) => (row.id === id ? updated : row));
  return updated;
}
