import { randomUUID } from 'node:crypto'

export const READY_MESSAGE = 'BOOKING_SERVER_E2E_READY_FOR_CREDENTIALS'
export const SANDBOX_BASE_URL = 'https://api.dev.kadicon.de/api'

export interface E2eEnvironment {
	GOOGLE_ACTION_CENTER_BOOKING_SERVER_USERNAME?: string
	GOOGLE_ACTION_CENTER_BOOKING_SERVER_PASSWORD?: string
	BOOKING_SERVER_E2E_MERCHANT_ID?: string
	BOOKING_SERVER_E2E_SERVICE_ID?: string
	BOOKING_SERVER_E2E_PARTY_SIZE?: string
	BOOKING_SERVER_E2E_DURATION_SEC?: string
}

interface SlotTime {
	service_id: string
	start_sec: string
	duration_sec: string
	resource_ids: { party_size: number }
}

interface Slot extends Omit<SlotTime, 'resource_ids'> {
	merchant_id: string
	resources: { party_size: number }
}

interface Booking {
	booking_id?: string
	slot?: Slot
	status?: string
}

interface BookingResponse {
	booking?: Booking
	booking_failure?: { cause?: string; description?: string }
}

interface AvailabilityResponse {
	slot_time_availability?: Array<{ slot_time?: SlotTime; available?: boolean }>
}

export interface ToolDependencies {
	fetch: typeof fetch
	log: (message: string) => void
	now: () => Date
	uuid: () => string
}

export function credentialsFromEnvironment(env: E2eEnvironment): { username: string; password: string } | undefined {
	const username = env.GOOGLE_ACTION_CENTER_BOOKING_SERVER_USERNAME?.trim()
	const password = env.GOOGLE_ACTION_CENTER_BOOKING_SERVER_PASSWORD?.trim()
	return username && password ? { username, password } : undefined
}

export function basicAuthHeader(credentials: { username: string; password: string }): string {
	return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`, 'utf8').toString('base64')}`
}

export function buildCreateBookingRequest(slot: Slot, idempotencyToken: string): Record<string, unknown> {
	return {
		slot,
		user_information: {
			user_id: `kadicon-sandbox-e2e-${idempotencyToken}`,
			given_name: 'Sandbox',
			family_name: 'E2E',
			telephone: '+49 000 000000',
			email: `booking-e2e+${idempotencyToken}@example.invalid`,
			language_code: 'de',
		},
		idempotency_token: idempotencyToken,
		additional_request: 'Synthetic KADiCon sandbox E2E booking; safe to cancel.',
	}
}

export function buildCandidateSlots(now: Date, serviceId: string, durationSec: string, partySize: number): SlotTime[] {
	const slots: SlotTime[] = []
	for (let dayOffset = 1; dayOffset <= 14; dayOffset++) {
		for (const hour of [12, 13, 18, 19, 20]) {
			const candidate = new Date(now)
			candidate.setUTCDate(candidate.getUTCDate() + dayOffset)
			candidate.setUTCHours(hour, 0, 0, 0)
			slots.push({
				service_id: serviceId,
				start_sec: String(Math.floor(candidate.getTime() / 1000)),
				duration_sec: durationSec,
				resource_ids: { party_size: partySize },
			})
		}
	}
	return slots
}

export async function runBookingServerE2e(
	env: E2eEnvironment,
	dependencies: ToolDependencies,
): Promise<'skipped' | 'completed'> {
	const credentials = credentialsFromEnvironment(env)
	if (!credentials) {
		dependencies.log(READY_MESSAGE)
		return 'skipped'
	}

	const merchantId = env.BOOKING_SERVER_E2E_MERCHANT_ID?.trim()
	const serviceId = env.BOOKING_SERVER_E2E_SERVICE_ID?.trim()
	if (!merchantId || !serviceId) {
		throw new Error('BOOKING_SERVER_E2E_MERCHANT_ID and BOOKING_SERVER_E2E_SERVICE_ID are required')
	}

	const partySize = Number(env.BOOKING_SERVER_E2E_PARTY_SIZE ?? '2')
	const durationSec = env.BOOKING_SERVER_E2E_DURATION_SEC ?? '7200'
	if (!Number.isInteger(partySize) || partySize < 1 || !/^\d+$/.test(durationSec) || durationSec === '0') {
		throw new Error('Party size and duration must be positive integers')
	}

	const authorization = basicAuthHeader(credentials)
	let step = 0
	const call = async <T>(endpoint: string, body: unknown, expectation: string): Promise<T> => {
		step++
		const response = await dependencies.fetch(`${SANDBOX_BASE_URL}${endpoint}`, {
			method: endpoint === '/v3/HealthCheck' ? 'GET' : 'POST',
			headers: { authorization, 'content-type': 'application/json' },
			body: endpoint === '/v3/HealthCheck' ? undefined : JSON.stringify(body),
		})
		const text = await response.text()
		dependencies.log(
			`STEP ${step} | ${endpoint} | HTTP ${response.status} | expected: ${expectation} | result: ${response.ok ? 'PASS' : 'FAIL'}`,
		)
		if (!response.ok) throw new Error(`${endpoint} returned HTTP ${response.status}`)
		return (text ? JSON.parse(text) : {}) as T
	}

	await call('/v3/HealthCheck', undefined, 'HTTP 2xx')
	const candidates = buildCandidateSlots(dependencies.now(), serviceId, durationSec, partySize)
	const availability = await call<AvailabilityResponse>(
		'/v3/BatchAvailabilityLookup',
		{ merchant_id: merchantId, slot_time: candidates },
		'one response per candidate and at least one available slot',
	)
	if (availability.slot_time_availability?.length !== candidates.length) {
		throw new Error('Availability response count does not match request count')
	}
	const availableSlot = availability.slot_time_availability.find((item) => item.available)?.slot_time
	if (!availableSlot) throw new Error('No available Durrani slot found in the next 14 days')

	const slot: Slot = {
		merchant_id: merchantId,
		service_id: availableSlot.service_id,
		start_sec: availableSlot.start_sec,
		duration_sec: availableSlot.duration_sec,
		resources: availableSlot.resource_ids,
	}
	const token = dependencies.uuid()
	const createRequest = buildCreateBookingRequest(slot, token)
	const created = await call<BookingResponse>('/v3/CreateBooking', createRequest, 'booking created')
	const retried = await call<BookingResponse>(
		'/v3/CreateBooking',
		createRequest,
		'same booking returned for identical token',
	)
	const bookingId = created.booking?.booking_id
	if (!bookingId || retried.booking?.booking_id !== bookingId) {
		throw new Error('CreateBooking idempotency check failed: booking IDs differ or are absent')
	}
	dependencies.log(`ASSERT | CreateBooking idempotency | expected: one booking ID | result: PASS`)

	await call('/v3/GetBookingStatus', { booking_id: bookingId }, 'CONFIRMED before modify')
	await call<BookingResponse>(
		'/v3/UpdateBooking',
		{ booking: { booking_id: bookingId, slot } },
		'valid modify succeeds without creating another booking',
	)
	await call('/v3/GetBookingStatus', { booking_id: bookingId }, 'CONFIRMED after modify')
	const cancelRequest = { booking: { booking_id: bookingId, status: 'CANCELED' } }
	await call<BookingResponse>('/v3/UpdateBooking', cancelRequest, 'booking becomes CANCELED')
	await call('/v3/GetBookingStatus', { booking_id: bookingId }, 'CANCELED')
	const secondCancel = await call<BookingResponse>(
		'/v3/UpdateBooking',
		cancelRequest,
		'idempotent repeat cancel returns the same CANCELED booking',
	)
	dependencies.log(
		`OBSERVATION | second cancel | expected: CANCELED | result: ${secondCancel.booking?.status === 'CANCELED' ? 'PASS' : 'FAIL'}`,
	)
	if (secondCancel.booking?.status !== 'CANCELED') throw new Error('Second cancel did not return CANCELED')

	const afterCancel = await call<AvailabilityResponse>(
		'/v3/BatchAvailabilityLookup',
		{ merchant_id: merchantId, slot_time: [availableSlot] },
		'original slot is available again after cancel',
	)
	const freeAgain = afterCancel.slot_time_availability?.[0]?.available === true
	dependencies.log(
		`ASSERT | availability after cancel | expected: available=true | result: ${freeAgain ? 'PASS' : 'FAIL'}`,
	)
	if (!freeAgain) throw new Error('Canceled slot was not reported available')
	return 'completed'
}

if (require.main === module) {
	runBookingServerE2e(process.env, { fetch, log: console.log, now: () => new Date(), uuid: randomUUID }).catch(
		(error: unknown) => {
			console.error(error instanceof Error ? error.message : 'Unknown booking-server E2E error')
			process.exitCode = 1
		},
	)
}
