import { OrgRoles } from "./authz/types.js";
import { DEFAULT_MEMBERSHIP_LIMIT } from "./entitlement-constants.js";
import { env } from "../env.js";
import { setEmailSendStatus } from "./email-send-status-store.js";
import { setPasswordResetLink } from "./password-reset-link-store.js";
import { createUserProfileIfNotExists } from "../data-access/runtime/userProfiles.js";
import { querySsoProviderIds } from "../data-access/runtime/auth.js";
import { extractCookieDomain, getInitialOrganization, getTrustedOrigins, hasCredentialAccount, shouldAutoProvision } from "./auth-config-utils.js";
import { checkPasswordPolicy } from "./password-policy.js";
import { ac, adminRole, memberRole, ownerRole } from "./permissions.js";
import { logSessionDeletion } from "./session-hooks.js";
import { maybeRediscoverSsoIssuer } from "./sso-issuer-discovery.js";
import { createAuthMiddleware } from "better-auth/api";
import { betterAuth } from "better-auth";
import { dash } from "@better-auth/infra";
import { oauthProvider } from "@better-auth/oauth-provider";
import { sso } from "@better-auth/sso";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { bearer, captcha, deviceAuthorization, haveIBeenPwned, jwt, lastLoginMethod, oAuthProxy, organization } from "better-auth/plugins";

//#region src/auth/auth.ts
const CAPTCHA_GUARDED_PATHS = [
	"/sign-up/email",
	"/sign-in/email",
	"/request-password-reset"
];
function createAuth(config) {
	const cookieDomain = extractCookieDomain(config.baseURL, config.cookieDomain);
	const isSecure = config.baseURL.startsWith("https://");
	const instance = betterAuth({
		appName: "Inkeep Agents",
		baseURL: config.baseURL,
		secret: config.secret,
		disabledPaths: ["/token"],
		database: ((options) => {
			const adapter = drizzleAdapter(config.dbClient, { provider: "pg" })(options);
			return {
				...adapter,
				create: (params) => {
					if (params.model === "organization" && params.data.id && !params.forceAllowId) return adapter.create({
						...params,
						forceAllowId: true
					});
					return adapter.create(params);
				}
			};
		}),
		emailAndPassword: {
			enabled: true,
			minPasswordLength: 15,
			maxPasswordLength: 256,
			requireEmailVerification: false,
			autoSignIn: true,
			resetPasswordTokenExpiresIn: 1800,
			sendResetPassword: async ({ user, url, token }) => {
				if (!await hasCredentialAccount(config.dbClient, user.id)) return;
				setPasswordResetLink({
					email: user.email,
					url,
					token
				});
				if (config.emailService?.isConfigured) try {
					await config.emailService.sendPasswordResetEmail({
						to: user.email,
						resetUrl: url
					});
				} catch (err) {
					console.error("[email] Failed to send password reset email:", err);
				}
			}
		},
		account: { accountLinking: {
			enabled: true,
			trustedProviders: async () => {
				const base = [
					"google",
					"microsoft",
					"email-password"
				];
				try {
					const providerIds = await querySsoProviderIds(config.dbClient)();
					return [...base, ...providerIds];
				} catch {
					return base;
				}
			}
		} },
		databaseHooks: { session: {
			create: { before: async (session) => {
				const organization$1 = await getInitialOrganization(config.dbClient, session.userId);
				return { data: {
					...session,
					activeOrganizationId: organization$1?.id ?? null
				} };
			} },
			delete: { after: async (session, context) => {
				await logSessionDeletion(session, context);
			} }
		} },
		socialProviders: (config.socialProviders?.google || config.socialProviders?.microsoft) && {
			...config.socialProviders?.google && { google: {
				...config.socialProviders.google,
				...env.OAUTH_PROXY_PRODUCTION_URL && { redirectURI: `${env.OAUTH_PROXY_PRODUCTION_URL}/api/auth/callback/google` }
			} },
			...config.socialProviders?.microsoft && { microsoft: {
				...config.socialProviders.microsoft,
				...env.OAUTH_PROXY_PRODUCTION_URL && { redirectURI: `${env.OAUTH_PROXY_PRODUCTION_URL}/api/auth/callback/microsoft` }
			} }
		},
		session: {
			storeSessionInDatabase: true,
			expiresIn: 3600 * 24 * 7,
			updateAge: 3600 * 24,
			cookieCache: {
				enabled: true,
				maxAge: 30,
				strategy: "compact"
			}
		},
		advanced: {
			...cookieDomain && { crossSubDomainCookies: {
				enabled: true,
				domain: cookieDomain
			} },
			defaultCookieAttributes: {
				httpOnly: true,
				...isSecure ? {
					sameSite: "none",
					secure: true
				} : {
					sameSite: "lax",
					secure: false
				},
				...cookieDomain && { domain: cookieDomain }
			},
			...config.advanced
		},
		trustedOrigins: (request) => getTrustedOrigins(config.dbClient, request),
		hooks: { before: createAuthMiddleware(async (ctx) => {
			await checkPasswordPolicy(ctx);
			const ssoRediscovery = await maybeRediscoverSsoIssuer(ctx, config.dbClient);
			if (ssoRediscovery) return ssoRediscovery;
			if (config.recaptcha && CAPTCHA_GUARDED_PATHS.includes(ctx.path) && ctx.headers?.get("x-captcha-response")) console.log("[captcha] pass", { path: ctx.path });
		}) },
		plugins: [
			bearer(),
			dash(),
			jwt(),
			oauthProvider({
				loginPage: `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/login`,
				consentPage: `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/consent`,
				scopes: [
					"openid",
					"profile",
					"email",
					"offline_access"
				],
				allowDynamicClientRegistration: true,
				allowUnauthenticatedClientRegistration: true,
				validAudiences: [
					config.baseURL,
					`${config.baseURL}/`,
					`${config.baseURL}/mcp`
				],
				customAccessTokenClaims: async ({ user }) => {
					if (!user) return {};
					const org = await getInitialOrganization(config.dbClient, user.id);
					return {
						"https://inkeep.com/tenantId": org?.id ?? void 0,
						"https://inkeep.com/email": user.email
					};
				}
			}),
			lastLoginMethod({ customResolveMethod(ctx) {
				const path = ctx.path;
				if (path === "/sign-in/email" || path === "/sign-up/email") return "email";
				if (path.startsWith("/callback/") || path.startsWith("/oauth2/callback/")) return ctx.params?.id || ctx.params?.providerId || path.split("/").pop() || null;
				if (path.startsWith("/sso/callback/")) return ctx.params?.providerId || path.split("/").pop() || null;
				if (path.startsWith("/sso/saml2/sp/acs/")) return ctx.params?.providerId || path.split("/").pop() || null;
				return null;
			} }),
			sso({
				organizationProvisioning: { disabled: true },
				async provisionUser({ user, provider }) {
					if (!provider.organizationId) return;
					if (!await shouldAutoProvision(config.dbClient, user, provider)) return;
					await instance.api.addMember({ body: {
						userId: user.id,
						organizationId: provider.organizationId,
						role: "member"
					} });
				}
			}),
			oAuthProxy({ productionURL: env.OAUTH_PROXY_PRODUCTION_URL || config.baseURL }),
			organization({
				allowUserToCreateOrganization: false,
				ac,
				roles: {
					member: memberRole,
					admin: adminRole,
					owner: ownerRole
				},
				creatorRole: OrgRoles.ADMIN,
				membershipLimit: async (_user, org) => {
					const { resolveTotalMembershipLimit } = await import("./entitlements.js");
					const { dalGetServiceAccountUserId } = await import("../data-access/runtime/entitlements.js");
					const serviceAccountUserId = await dalGetServiceAccountUserId(config.dbClient, org.id);
					return resolveTotalMembershipLimit(config.dbClient, org.id, !!serviceAccountUserId);
				},
				invitationLimit: DEFAULT_MEMBERSHIP_LIMIT,
				invitationExpiresIn: 10080 * 60,
				async sendInvitationEmail(data) {
					if (config.emailService?.isConfigured) try {
						const invitationUrl = `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/accept-invitation/${data.id}?email=${encodeURIComponent(data.email)}`;
						const result = await config.emailService.sendInvitationEmail({
							to: data.email,
							inviterName: data.inviter.user.name || data.inviter.user.email,
							organizationName: data.organization.name,
							role: data.role,
							invitationUrl,
							authMethod: data.invitation?.authMethod
						});
						setEmailSendStatus(data.id, {
							emailSent: result.emailSent,
							error: result.error,
							organizationId: data.organization.id
						});
					} catch (err) {
						const message = err instanceof Error ? err.message : String(err);
						console.error(`[email] Failed to send invitation email to ${data.email}:`, message);
						setEmailSendStatus(data.id, {
							emailSent: false,
							error: message,
							organizationId: data.organization.id
						});
					}
					else setEmailSendStatus(data.id, {
						emailSent: false,
						organizationId: data.organization.id
					});
				},
				schema: {
					invitation: { additionalFields: { authMethod: {
						type: "string",
						required: false
					} } },
					organization: { additionalFields: {
						preferredAuthMethod: {
							type: "string",
							input: true,
							required: false
						},
						allowedAuthMethods: {
							type: "string",
							input: true,
							required: false
						},
						serviceAccountUserId: {
							type: "string",
							input: true,
							required: false
						}
					} }
				},
				organizationHooks: {
					beforeCreateOrganization: async ({ organization: org }) => {
						return { data: {
							...org,
							id: org.slug
						} };
					},
					beforeCreateInvitation: async ({ invitation, organization: org }) => {
						const { enforcePerRoleSeatLimit } = await import("./entitlements.js");
						await enforcePerRoleSeatLimit(config.dbClient, org.id, invitation.role);
					},
					beforeAddMember: async ({ member, user, organization: org }) => {
						const { enforcePerRoleSeatLimit } = await import("./entitlements.js");
						await enforcePerRoleSeatLimit(config.dbClient, org.id, member.role);
						try {
							const { syncOrgMemberToSpiceDb } = await import("./authz/sync.js");
							await syncOrgMemberToSpiceDb({
								tenantId: org.id,
								userId: user.id,
								role: member.role,
								action: "add"
							});
							console.log(`🔐 SpiceDB: Synced member ${user.email} as ${member.role} to org ${org.name}`);
						} catch (error) {
							console.error("❌ SpiceDB sync failed for new member:", error);
							throw new Error(`Failed to sync member permissions: ${error instanceof Error ? error.message : "Unknown error"}`);
						}
					},
					beforeAcceptInvitation: async ({ invitation, user, organization: org }) => {
						const { enforcePerRoleSeatLimit } = await import("./entitlements.js");
						await enforcePerRoleSeatLimit(config.dbClient, org.id, invitation.role);
						try {
							const { syncOrgMemberToSpiceDb } = await import("./authz/sync.js");
							await syncOrgMemberToSpiceDb({
								tenantId: org.id,
								userId: user.id,
								role: invitation.role,
								action: "add"
							});
							console.log(`🔐 SpiceDB: Synced member ${user.email} as ${invitation.role} to org ${org.name}`);
						} catch (error) {
							console.error("❌ SpiceDB sync failed for new member:", error);
							throw new Error(`Failed to sync member permissions: ${error instanceof Error ? error.message : "Unknown error"}`);
						}
						try {
							await createUserProfileIfNotExists(config.dbClient)(user.id);
						} catch (error) {
							console.error("[auth] Failed to create user profile for user", user.id, error);
						}
						if (invitation.role !== OrgRoles.ADMIN && invitation.role !== OrgRoles.OWNER) {
							const { getProjectAssignmentsForInvitation, deleteInvitationProjectAssignments } = await import("../data-access/runtime/invitationProjectAssignments.js");
							const { grantProjectAccess } = await import("./authz/sync.js");
							const assignments = await getProjectAssignmentsForInvitation(config.dbClient)(invitation.id);
							if (assignments.length > 0) (await Promise.allSettled(assignments.map((a) => grantProjectAccess({
								tenantId: org.id,
								projectId: a.projectId,
								userId: user.id,
								role: a.projectRole
							})))).forEach((result, i) => {
								if (result.status === "rejected") console.warn(`[auth] Failed to grant project access for project ${assignments[i]?.projectId}:`, result.reason);
							});
							await deleteInvitationProjectAssignments(config.dbClient)(invitation.id);
						}
					},
					beforeUpdateMemberRole: async ({ member, organization: org, newRole }) => {
						const { roleMatchesAdminBucket, enforcePerRoleSeatLimit } = await import("./entitlements.js");
						const oldRole = member.role;
						const targetRole = newRole;
						const { changeOrgRole, revokeAllProjectMemberships } = await import("./authz/sync.js");
						const doRoleChange = async () => {
							await changeOrgRole({
								tenantId: org.id,
								userId: member.userId,
								oldRole,
								newRole: targetRole
							});
						};
						if (roleMatchesAdminBucket(oldRole) !== roleMatchesAdminBucket(targetRole)) await enforcePerRoleSeatLimit(config.dbClient, org.id, targetRole, doRoleChange);
						else await doRoleChange();
						console.log(`🔐 SpiceDB: Updated member ${member.userId} role from ${oldRole} to ${targetRole} in org ${org.name}`);
						if (oldRole === OrgRoles.MEMBER && (targetRole === OrgRoles.ADMIN || targetRole === OrgRoles.OWNER)) {
							await revokeAllProjectMemberships({
								tenantId: org.id,
								userId: member.userId
							});
							console.log(`🔐 SpiceDB: Revoked all project memberships for ${member.userId} (promoted to ${targetRole})`);
						}
					},
					beforeRemoveMember: async ({ member, organization: org }) => {
						try {
							if (config.manageDbPool) {
								const { cleanupUserTriggers } = await import("../data-access/manage/triggerCleanup.js");
								await cleanupUserTriggers({
									tenantId: org.id,
									userId: member.userId,
									runDb: config.dbClient,
									manageDbPool: config.manageDbPool
								});
							}
							const { revokeAllUserRelationships } = await import("./authz/sync.js");
							await revokeAllUserRelationships({
								tenantId: org.id,
								userId: member.userId
							});
							console.log(`🔐 Preparing to remove member ${member.userId} - cleaned up triggers and revoked all relationships in org ${org.name}`);
						} catch (error) {
							console.error("❌ Cleanup failed before member removal:", error);
							throw new Error(`Failed to clean up user data: ${error instanceof Error ? error.message : "Unknown error"}`);
						}
					}
				}
			}),
			deviceAuthorization({
				verificationUri: "/device",
				expiresIn: "60m",
				interval: "5s",
				userCodeLength: 8
			}),
			haveIBeenPwned({ customPasswordCompromisedMessage: "Please choose a more secure password." }),
			...config.recaptcha ? [captcha({
				provider: "google-recaptcha",
				secretKey: config.recaptcha.secretKey,
				minScore: config.recaptcha.minScore ?? .5
			})] : []
		]
	});
	return instance;
}
const auth = null;

//#endregion
export { auth, createAuth, extractCookieDomain, hasCredentialAccount };