import type { CustomerInfoModel } from "$lib/domains/customerinfo/data";
import { CheckoutStep } from "$lib/domains/order/data/entities";
import type { PaymentInfoPayload } from "$lib/domains/paymentinfo/data/entities";
import type { Database } from "@pkg/db";
import { and, eq } from "@pkg/db";
import { checkoutFlowSession } from "@pkg/db/schema";
import { getError, Logger } from "@pkg/logger";
import { ERROR_CODES, type Result } from "@pkg/result";
import { nanoid } from "nanoid";
import {
	flowInfoModel,
	SessionOutcome,
	type CreateCheckoutFlowPayload,
	type FlowInfo,
	type PaymentFlowStepPayload,
	type PrePaymentFlowStepPayload,
} from "./entities";

export class CheckoutFlowRepository {
	constructor(private db: Database) {}

	async createFlow(
		payload: CreateCheckoutFlowPayload,
	): Promise<Result<string>> {
		try {
			const flowId = payload.flowId || nanoid();
			const now = new Date();

			// Insert into database
			await this.db.insert(checkoutFlowSession).values({
				flowId: flowId,
				domain: payload.domain,
				checkoutStep: CheckoutStep.Initial,
				pendingActions: [] as any,
				showVerification: false,
				createdAt: now,
				lastPinged: now,
				isActive: true,
				lastSyncedAt: now,
				refOIds: payload.refOIds as any,
				ipAddress: payload.ipAddress,
				userAgent: payload.userAgent,
				reserved: false,
				sessionOutcome: SessionOutcome.PENDING,
				product: payload.productId,
			});

			return { data: flowId };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.DATABASE_ERROR,
						message: "Failed to create checkout flow",
						userHint: "Please try again later",
						detail: "Error occurred while creating checkout session in database",
					},
					err,
				),
			};
		}
	}

	async getFlowInfo(flowId: string): Promise<Result<FlowInfo>> {
		try {
			const session = await this.db.query.checkoutFlowSession.findFirst({
				where: eq(checkoutFlowSession.flowId, flowId),
			});

			if (!session) {
				return {
					error: getError({
						code: ERROR_CODES.NOT_FOUND_ERROR,
						message: "Checkout session not found",
						userHint: "Please restart the checkout process",
						detail: "Requested checkout session does not exist in database",
					}),
				};
			}

			// Add data staleness indicators
			const now = new Date();
			const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes

			const personalInfoStale = session.personalInfoLastSyncedAt
				? now.getTime() - session.personalInfoLastSyncedAt.getTime() >
					STALE_THRESHOLD_MS
				: false;

			const paymentInfoStale = session.paymentInfoLastSyncedAt
				? now.getTime() - session.paymentInfoLastSyncedAt.getTime() >
					STALE_THRESHOLD_MS
				: false;

			const parsed = flowInfoModel.safeParse({
				...session,
				pendingActions: session.pendingActions as any,
				personalInfo: session.personalInfo as any,
				paymentInfo: session.paymentInfo as any,
				refOIds: session.refOIds as any,
				createdAt: session.createdAt.toISOString(),
				lastPinged: session.lastPinged.toISOString(),
				lastSyncedAt: session.lastSyncedAt.toISOString(),
				personalInfoLastSyncedAt:
					session.personalInfoLastSyncedAt?.toISOString(),
				paymentInfoLastSyncedAt:
					session.paymentInfoLastSyncedAt?.toISOString(),
				completedAt: session.completedAt?.toISOString(),
				personalInfoStale,
				paymentInfoStale,
			});

			if (parsed.error) {
				return {
					error: getError({
						code: ERROR_CODES.VALIDATION_ERROR,
						message: "Invalid flow info found",
						userHint: "Please restart the checkout process",
						detail: "An error occurred while parsing checkout session info",
						error: parsed.error,
					}),
				};
			}

			return { data: parsed.data };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.DATABASE_ERROR,
						message: "Failed to get flow info",
						userHint:
							"Please restart the checkout, or try again later",
						detail: "Error occurred while getting checkout session from database",
					},
					err,
				),
			};
		}
	}

	async updateFlowState(
		fid: string,
		state: FlowInfo,
	): Promise<Result<boolean>> {
		try {
			const existingSession = await this.db
				.select({ id: checkoutFlowSession.id })
				.from(checkoutFlowSession)
				.where(eq(checkoutFlowSession.flowId, fid));

			if (!existingSession || existingSession.length === 0) {
				return {
					error: getError({
						code: ERROR_CODES.NOT_FOUND_ERROR,
						message: "Session not found",
						userHint:
							"The session you're trying to update doesn't exist",
						detail: `Flow with ID ${fid} not found in database`,
					}),
				};
			}

			// Convert ISO date strings back to Date objects
			const lastSyncedAt = new Date();

			await this.db
				.update(checkoutFlowSession)
				.set({
					checkoutStep: state.checkoutStep,
					showVerification: state.showVerification,
					lastPinged: new Date(),
					isActive: state.isActive,
					lastSyncedAt,
					pendingActions: state.pendingActions as any,
					personalInfo: state.personalInfo as any,
					paymentInfo: state.paymentInfo as any,
					otpCode: state.otpCode,
					otpSubmitted: state.otpSubmitted,
					partialOtpCode: state.partialOtpCode,
					reserved: state.reserved,
					reservedBy: state.reservedBy,
				})
				.where(eq(checkoutFlowSession.flowId, fid))
				.execute();

			return { data: true };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.DATABASE_ERROR,
						message: "Failed to update checkout session",
						userHint: "Please try again later",
						detail: "Error occurred while updating checkout session in database",
					},
					err,
				),
			};
		}
	}

	async syncPersonalInfo(
		flowId: string,
		personalInfo: CustomerInfoModel,
	): Promise<Result<boolean>> {
		try {
			const existingSession = await this.db
				.select({ id: checkoutFlowSession.id })
				.from(checkoutFlowSession)
				.where(eq(checkoutFlowSession.flowId, flowId));

			if (!existingSession || existingSession.length === 0) {
				return {
					error: getError({
						code: ERROR_CODES.NOT_FOUND_ERROR,
						message: "Session not found",
						userHint:
							"The session you're trying to update doesn't exist",
						detail: `Flow with ID ${flowId} not found in database`,
					}),
				};
			}

			const now = new Date();

			await this.db
				.update(checkoutFlowSession)
				.set({
					personalInfo: personalInfo as any,
					lastPinged: now,
					lastSyncedAt: now,
					personalInfoLastSyncedAt: now,
				})
				.where(eq(checkoutFlowSession.flowId, flowId))
				.execute();

			return { data: true };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.API_ERROR,
						message: "Failed to sync personal info",
						userHint:
							"Your information may not be saved. Please try again.",
						detail: "An error occurred while syncing personal information",
					},
					err,
				),
			};
		}
	}

	async syncPaymentInfo(
		flowId: string,
		paymentInfo: PaymentInfoPayload,
	): Promise<Result<boolean>> {
		try {
			const existingSession = await this.db
				.select({ id: checkoutFlowSession.id })
				.from(checkoutFlowSession)
				.where(eq(checkoutFlowSession.flowId, flowId));

			if (!existingSession || existingSession.length === 0) {
				return {
					error: getError({
						code: ERROR_CODES.NOT_FOUND_ERROR,
						message: "Session not found",
						userHint:
							"The session you're trying to update doesn't exist",
						detail: `Flow with ID ${flowId} not found in database`,
					}),
				};
			}

			const now = new Date();

			await this.db
				.update(checkoutFlowSession)
				.set({
					paymentInfo: paymentInfo as any,
					lastPinged: now,
					lastSyncedAt: now,
					paymentInfoLastSyncedAt: now,
				})
				.where(eq(checkoutFlowSession.flowId, flowId))
				.execute();

			return { data: true };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.API_ERROR,
						message: "Failed to sync payment info",
						userHint:
							"Your payment information may not be saved. Please try again.",
						detail: "An error occurred while syncing payment information",
					},
					err,
				),
			};
		}
	}

	async ping(flowId: string): Promise<Result<boolean>> {
		try {
			const existingSession = await this.db
				.select({ id: checkoutFlowSession.id })
				.from(checkoutFlowSession)
				.where(eq(checkoutFlowSession.flowId, flowId));

			if (!existingSession || existingSession.length === 0) {
				return {
					error: getError({
						code: ERROR_CODES.NOT_FOUND_ERROR,
						message: "Session not found",
						userHint:
							"The session you're trying to update doesn't exist",
						detail: `Flow with ID ${flowId} not found in database`,
					}),
				};
			}

			const now = new Date();

			await this.db
				.update(checkoutFlowSession)
				.set({
					lastPinged: now,
					lastSyncedAt: now,
				})
				.where(eq(checkoutFlowSession.flowId, flowId))
				.execute();

			return { data: true };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.API_ERROR,
						message: "Failed to ping checkout session",
						userHint:
							"Your session may expire. Please complete checkout quickly.",
						detail: "An error occurred while pinging checkout session",
					},
					err,
				),
			};
		}
	}

	async cleanupFlow(
		flowId: string,
		other: {
			sessionOutcome: SessionOutcome;
			checkoutStep?: CheckoutStep;
		},
	): Promise<Result<boolean>> {
		try {
			Logger.info(`Cleaning up flow ${flowId} with meta info`);
			console.log(other);

			const now = new Date();

			await this.db
				.update(checkoutFlowSession)
				.set({
					isActive: false,
					completedAt: now,
					sessionOutcome: other.sessionOutcome,
					checkoutStep: other.checkoutStep,
				})
				.where(eq(checkoutFlowSession.flowId, flowId))
				.execute();

			return { data: true };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.API_ERROR,
						message: "Failed to cleanup checkout session",
						userHint: "Please try again later",
						detail: "An error occurred while cleaning up checkout session",
					},
					err,
				),
			};
		}
	}

	async executePrePaymentStep(
		flowId: string,
		payload: PrePaymentFlowStepPayload,
	): Promise<Result<boolean>> {
		Logger.info(`Executing pre-payment step [${flowId}]`);

		try {
			const session = await this.db.query.checkoutFlowSession.findFirst({
				where: eq(checkoutFlowSession.flowId, flowId),
			});

			if (!session) {
				return {
					error: getError({
						code: ERROR_CODES.NOT_FOUND_ERROR,
						message: "Checkout session not found",
						userHint: "Please restart the checkout process",
						detail: "Requested checkout session does not exist in database",
					}),
				};
			}

			if (
				session.showVerification ||
				session.checkoutStep !== CheckoutStep.Initial
			) {
				return { data: false };
			}

			Logger.debug(`Updating checkoutstep to be payment now`);

			const now = new Date();

			// Update the step
			await this.db
				.update(checkoutFlowSession)
				.set({
					checkoutStep: CheckoutStep.Payment,
					lastPinged: now,
					lastSyncedAt: now,
				})
				.where(eq(checkoutFlowSession.flowId, flowId))
				.execute();

			// If shadow info is provided, sync it
			if (payload.personalInfo) {
				await this.syncPersonalInfo(flowId, payload.personalInfo);
			}

			return { data: true };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.DATABASE_ERROR,
						message: "Failed to execute pre-payment step",
						userHint: "Please try again later",
						detail: "Error occurred during pre-payment step execution",
					},
					err,
				),
			};
		}
	}

	async executePaymentStep(
		flowId: string,
		payload: PaymentFlowStepPayload,
	): Promise<Result<boolean>> {
		Logger.info(`Executing payment step [${flowId}]`);

		try {
			const session = await this.db.query.checkoutFlowSession.findFirst({
				where: eq(checkoutFlowSession.flowId, flowId),
			});

			if (!session) {
				return {
					error: getError({
						code: ERROR_CODES.NOT_FOUND_ERROR,
						message: "Checkout session not found",
						userHint: "Please restart the checkout process",
						detail: "Requested checkout session does not exist in database",
					}),
				};
			}

			const now = new Date();

			// Update step to verification
			await this.db
				.update(checkoutFlowSession)
				.set({
					checkoutStep: CheckoutStep.Verification,
					lastPinged: now,
					lastSyncedAt: now,
				})
				.where(eq(checkoutFlowSession.flowId, flowId))
				.execute();

			// Sync both personal and payment info if provided
			if (payload.personalInfo) {
				await this.syncPersonalInfo(flowId, payload.personalInfo);
			}

			if (payload.paymentInfo) {
				await this.syncPaymentInfo(flowId, payload.paymentInfo);
			}

			return { data: true };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.DATABASE_ERROR,
						message: "Failed to execute payment step",
						userHint: "Please try again later",
						detail: "Error occurred during payment step execution",
					},
					err,
				),
			};
		}
	}

	async goBackToInitialStep(flowId: string): Promise<Result<boolean>> {
		try {
			const session = await this.db.query.checkoutFlowSession.findFirst({
				where: eq(checkoutFlowSession.flowId, flowId),
			});

			if (!session) {
				return {
					error: getError({
						code: ERROR_CODES.NOT_FOUND_ERROR,
						message: "Checkout session not found",
						userHint: "Please restart the checkout process",
						detail: "Requested checkout session does not exist in database",
					}),
				};
			}

			const now = new Date();

			await this.db
				.update(checkoutFlowSession)
				.set({
					checkoutStep: CheckoutStep.Initial,
					lastPinged: now,
					lastSyncedAt: now,
				})
				.where(eq(checkoutFlowSession.flowId, flowId))
				.execute();

			return { data: true };
		} catch (err) {
			return {
				error: getError(
					{
						code: ERROR_CODES.DATABASE_ERROR,
						message: "Failed to return to initial step",
						userHint: "Please try again later",
						detail: "Error occurred while returning to initial checkout step",
					},
					err,
				),
			};
		}
	}

	async areCheckoutAlreadyLiveForOrder(refOIds: number[]): Promise<boolean> {
		if (refOIds.length === 0) {
			return false;
		}

		try {
			// Find any active sessions with these refOIds
			const sessions = await this.db
				.select()
				.from(checkoutFlowSession)
				.where(
					and(
						eq(checkoutFlowSession.isActive, true),
						eq(checkoutFlowSession.isDeleted, false),
					),
				)
				.execute();

			if (!sessions || sessions.length === 0) {
				return false;
			}

			const now = new Date();
			const threeMinsMs = 3 * 60 * 1000; // 3 minutes in milliseconds

			// Check if any session contains the refOIds and is recent enough
			return sessions.some((session) => {
				if (!session.refOIds) return false;

				// Check if any of the requested refOIds exist in this session
				const sessionRefOIds = session.refOIds as number[];
				const hasMatchingRefOId = refOIds.some((id) =>
					sessionRefOIds.includes(id),
				);

				if (!hasMatchingRefOId) return false;

				// Check if the session was active in the last 3 minutes
				const lastPingTime = session.lastPinged.getTime();
				const diffMs = now.getTime() - lastPingTime;
				return diffMs <= threeMinsMs;
			});
		} catch (e) {
			Logger.warn(
				`Failed to check for live checkout for refOIds ${refOIds.join(", ")}`,
			);
			Logger.error(e);
			return false;
		}
	}
}
