import type { BetterAuthOptions } from "../types";
import { resolveDatabaseSchemaIndexes } from "./database-index";
import type {
	BetterAuthDBSchema,
	DBFieldAttribute,
	DBTableIndex,
} from "./type";

function mergeTableIndexes(
	...indexCollections: ReadonlyArray<readonly DBTableIndex[] | undefined>
) {
	const indexes: DBTableIndex[] = [];
	const seenIndexDefinitions = new Set<string>();
	for (const index of indexCollections.flatMap(
		(collection) => collection ?? [],
	)) {
		const definition = JSON.stringify([
			index.name ?? null,
			index.fields,
			index.unique ?? false,
		]);
		if (seenIndexDefinitions.has(definition)) continue;
		seenIndexDefinitions.add(definition);
		indexes.push(index);
	}
	return indexes;
}

const buildAuthTables = (options: BetterAuthOptions): BetterAuthDBSchema => {
	const pluginSchema = (options.plugins ?? []).reduce(
		(acc, plugin) => {
			const schema = plugin.schema;
			if (!schema) return acc;
			for (const [key, value] of Object.entries(schema)) {
				acc[key] = {
					fields: {
						...acc[key]?.fields,
						...value.fields,
					},
					indexes: mergeTableIndexes(acc[key]?.indexes, value.indexes),
					modelName: value.modelName || key,
					disableMigrations:
						value.disableMigration ?? acc[key]?.disableMigrations,
				};
			}
			return acc;
		},
		{} as Record<
			string,
			{
				fields: Record<string, DBFieldAttribute>;
				indexes?: readonly DBTableIndex[] | undefined;
				modelName: string;
				disableMigrations?: boolean | undefined;
			}
		>,
	);

	const shouldAddRateLimitTable = options.rateLimit?.storage === "database";
	const rateLimitTable = {
		rateLimit: {
			modelName: options.rateLimit?.modelName || "rateLimit",
			fields: {
				key: {
					type: "string",
					unique: true,
					required: true,
					fieldName: options.rateLimit?.fields?.key || "key",
				},
				count: {
					type: "number",
					required: true,
					fieldName: options.rateLimit?.fields?.count || "count",
				},
				lastRequest: {
					type: "number",
					bigint: true,
					required: true,
					fieldName: options.rateLimit?.fields?.lastRequest || "lastRequest",
					defaultValue: () => Date.now(),
				},
			},
		},
	} satisfies BetterAuthDBSchema;

	const { user, session, account, verification, ...pluginTables } =
		pluginSchema;

	const verificationTable = {
		verification: {
			modelName: options.verification?.modelName || "verification",
			indexes: verification?.indexes,
			fields: {
				identifier: {
					type: "string",
					required: true,
					fieldName: options.verification?.fields?.identifier || "identifier",
					index: true,
				},
				value: {
					type: "string",
					required: true,
					fieldName: options.verification?.fields?.value || "value",
				},
				expiresAt: {
					type: "date",
					required: true,
					fieldName: options.verification?.fields?.expiresAt || "expiresAt",
				},
				createdAt: {
					type: "date",
					required: true,
					defaultValue: () => new Date(),
					fieldName: options.verification?.fields?.createdAt || "createdAt",
				},
				updatedAt: {
					type: "date",
					required: true,
					defaultValue: () => new Date(),
					onUpdate: () => new Date(),
					fieldName: options.verification?.fields?.updatedAt || "updatedAt",
				},
				...verification?.fields,
				...options.verification?.additionalFields,
			},
			order: 4,
		},
	} satisfies BetterAuthDBSchema;

	const sessionTable = {
		session: {
			modelName: options.session?.modelName || "session",
			indexes: session?.indexes,
			fields: {
				expiresAt: {
					type: "date",
					required: true,
					fieldName: options.session?.fields?.expiresAt || "expiresAt",
				},
				token: {
					type: "string",
					required: true,
					fieldName: options.session?.fields?.token || "token",
					unique: true,
				},
				createdAt: {
					type: "date",
					required: true,
					fieldName: options.session?.fields?.createdAt || "createdAt",
					defaultValue: () => new Date(),
				},
				updatedAt: {
					type: "date",
					required: true,
					fieldName: options.session?.fields?.updatedAt || "updatedAt",
					onUpdate: () => new Date(),
				},
				ipAddress: {
					type: "string",
					required: false,
					fieldName: options.session?.fields?.ipAddress || "ipAddress",
				},
				userAgent: {
					type: "string",
					required: false,
					fieldName: options.session?.fields?.userAgent || "userAgent",
				},
				userId: {
					type: "string",
					fieldName: options.session?.fields?.userId || "userId",
					references: {
						// Use the canonical user schema key here rather than
						// `options.user.modelName`. Downstream consumers (e.g.
						// `getSchema`, `getMigrations`, and the runtime adapter
						// resolvers) treat `references.model` as a schema key
						// and look it up via `tables[references.model]` /
						// `getDefaultModelName`. Writing the modelName alias
						// here would collide when a user picks a modelName that
						// matches another schema key (for example
						// `user.modelName = "account"`), causing the FK to
						// resolve to the wrong table.
						// @see https://github.com/better-auth/better-auth/issues/8111
						model: "user",
						field: "id",
						onDelete: "cascade",
					},
					required: true,
					index: true,
				},
				...session?.fields,
				...options.session?.additionalFields,
			},
			order: 2,
		},
	} satisfies BetterAuthDBSchema;

	const authTables = {
		user: {
			modelName: options.user?.modelName || "user",
			indexes: user?.indexes,
			fields: {
				name: {
					type: "string",
					required: true,
					fieldName: options.user?.fields?.name || "name",
					sortable: true,
				},
				email: {
					type: "string",
					// TODO(#9124): drop required+unique in v2; use a partial unique
					// index where email is not null (see schema/user.ts).
					unique: true,
					required: true,
					fieldName: options.user?.fields?.email || "email",
					sortable: true,
				},
				emailVerified: {
					type: "boolean",
					defaultValue: false,
					required: true,
					fieldName: options.user?.fields?.emailVerified || "emailVerified",
					input: false,
				},
				image: {
					type: "string",
					required: false,
					fieldName: options.user?.fields?.image || "image",
				},
				createdAt: {
					type: "date",
					defaultValue: () => new Date(),
					required: true,
					fieldName: options.user?.fields?.createdAt || "createdAt",
				},
				updatedAt: {
					type: "date",
					defaultValue: () => new Date(),
					onUpdate: () => new Date(),
					required: true,
					fieldName: options.user?.fields?.updatedAt || "updatedAt",
				},
				...user?.fields,
				...options.user?.additionalFields,
			},
			order: 1,
		},
		//only add session table if it's not stored in secondary storage
		...(!options.secondaryStorage || options.session?.storeSessionInDatabase
			? sessionTable
			: {}),
		account: {
			modelName: options.account?.modelName || "account",
			indexes: mergeTableIndexes(
				[
					{
						fields: ["issuer", "accountId"],
						unique: true,
					},
				],
				account?.indexes,
			),
			fields: {
				issuer: {
					type: "string",
					required: true,
					fieldName: options.account?.fields?.issuer || "issuer",
				},
				accountId: {
					type: "string",
					required: true,
					fieldName: options.account?.fields?.accountId || "accountId",
				},
				providerId: {
					type: "string",
					required: true,
					fieldName: options.account?.fields?.providerId || "providerId",
				},
				userId: {
					type: "string",
					references: {
						// See note on `session.userId.references.model` above:
						// always use the canonical user schema key so the FK
						// target survives `user.modelName` aliasing.
						// @see https://github.com/better-auth/better-auth/issues/8111
						model: "user",
						field: "id",
						onDelete: "cascade",
					},
					required: true,
					fieldName: options.account?.fields?.userId || "userId",
					index: true,
				},
				accessToken: {
					type: "string",
					required: false,
					returned: false,
					fieldName: options.account?.fields?.accessToken || "accessToken",
				},
				refreshToken: {
					type: "string",
					required: false,
					returned: false,
					fieldName: options.account?.fields?.refreshToken || "refreshToken",
				},
				idToken: {
					type: "string",
					required: false,
					returned: false,
					fieldName: options.account?.fields?.idToken || "idToken",
				},
				accessTokenExpiresAt: {
					type: "date",
					required: false,
					returned: false,
					fieldName:
						options.account?.fields?.accessTokenExpiresAt ||
						"accessTokenExpiresAt",
				},
				refreshTokenExpiresAt: {
					type: "date",
					required: false,
					returned: false,
					fieldName:
						options.account?.fields?.refreshTokenExpiresAt ||
						"refreshTokenExpiresAt",
				},
				scope: {
					type: "string",
					required: false,
					fieldName: options.account?.fields?.scope || "scope",
				},
				password: {
					type: "string",
					required: false,
					returned: false,
					fieldName: options.account?.fields?.password || "password",
				},
				createdAt: {
					type: "date",
					required: true,
					fieldName: options.account?.fields?.createdAt || "createdAt",
					defaultValue: () => new Date(),
				},
				updatedAt: {
					type: "date",
					required: true,
					fieldName: options.account?.fields?.updatedAt || "updatedAt",
					onUpdate: () => new Date(),
				},
				...account?.fields,
				...options.account?.additionalFields,
			},
			order: 3,
		},
		...(!options.secondaryStorage || options.verification?.storeInDatabase
			? verificationTable
			: {}),
		...pluginTables,
		...(shouldAddRateLimitTable ? rateLimitTable : {}),
	} satisfies BetterAuthDBSchema;

	return authTables;
};

export function getAuthTablesWithResolvedIndexes(options: BetterAuthOptions) {
	const tables = buildAuthTables(options);
	const indexesByTable = resolveDatabaseSchemaIndexes(
		Object.values(tables)
			.filter(
				(table) => !("disableMigrations" in table) || !table.disableMigrations,
			)
			.map((table) => ({
				fields: table.fields,
				indexes: "indexes" in table ? table.indexes : undefined,
				tableName: table.modelName,
			})),
	);

	return { indexesByTable, tables };
}

export const getAuthTables = (options: BetterAuthOptions): BetterAuthDBSchema =>
	getAuthTablesWithResolvedIndexes(options).tables;
