import type { BetterAuthClientPlugin } from "@better-auth/core";
import type { DBFieldAttribute } from "@better-auth/core/db";
import { atom } from "nanostores";
import { useAuthQuery } from "../../client";
import type {
	InferInvitation,
	InferMember,
	InferOrganization,
	InferTeam,
	Member,
} from "../../plugins/organization/schema";
import type { BetterAuthOptions, BetterAuthPlugin } from "../../types";
import type { Prettify } from "../../types/helper";
import { PACKAGE_VERSION } from "../../version";
import type { AccessControl, ArrayElement, Role } from "../access";
import type { defaultStatements } from "./access";
import { adminAc, defaultRoles, memberAc, ownerAc } from "./access";
import { ORGANIZATION_ERROR_CODES } from "./error-codes";
import type { OrganizationPlugin } from "./organization";
import type { HasPermissionBaseInput } from "./permission";
import { hasPermissionFn } from "./permission";
import type { OrganizationOptions } from "./types";

export * from "./error-codes";

/**
 * Using the same `hasPermissionFn` function, but without the need for a `ctx` parameter or the `organizationId` parameter.
 */
export const clientSideHasPermission = (input: HasPermissionBaseInput) => {
	const acRoles: {
		[x: string]: Role<any> | undefined;
	} = input.options.roles || defaultRoles;

	return hasPermissionFn(input, acRoles);
};

interface OrganizationClientOptions {
	ac?: AccessControl | undefined;
	roles?:
		| {
				[key in string]: Role;
		  }
		| undefined;
	teams?:
		| {
				enabled: boolean;
		  }
		| undefined;
	schema?:
		| {
				organization?: {
					additionalFields?: {
						[key: string]: DBFieldAttribute;
					};
				};
				member?: {
					additionalFields?: {
						[key: string]: DBFieldAttribute;
					};
				};
				invitation?: {
					additionalFields?: {
						[key: string]: DBFieldAttribute;
					};
				};
				team?: {
					additionalFields?: {
						[key: string]: DBFieldAttribute;
					};
				};
				organizationRole?: {
					additionalFields?: {
						[key: string]: DBFieldAttribute;
					};
				};
		  }
		| undefined;
	dynamicAccessControl?:
		| {
				enabled: boolean;
		  }
		| undefined;
}

export const organizationClient = <CO extends OrganizationClientOptions>(
	options?: CO | undefined,
) => {
	const $listOrg = atom<boolean>(false);
	const $activeOrgSignal = atom<boolean>(false);
	const $activeMemberSignal = atom<boolean>(false);
	const $activeMemberRoleSignal = atom<boolean>(false);

	type DefaultStatements = typeof defaultStatements;
	type Statements =
		CO["ac"] extends AccessControl<infer S> ? S : DefaultStatements;
	type PermissionType = {
		[key in keyof Statements]?: Array<
			Statements[key] extends readonly unknown[]
				? ArrayElement<Statements[key]>
				: never
		>;
	};
	type PermissionExclusive = {
		permissions: PermissionType;
	};

	const roles = {
		admin: adminAc,
		member: memberAc,
		owner: ownerAc,
		...options?.roles,
	};

	type OrganizationReturn = CO["teams"] extends { enabled: true }
		? {
				members: InferMember<CO>[];
				invitations: InferInvitation<CO>[];
				teams: InferTeam<CO>[];
			} & InferOrganization<CO>
		: {
				members: InferMember<CO>[];
				invitations: InferInvitation<CO>[];
			} & InferOrganization<CO>;

	type Schema = CO["schema"];
	return {
		id: "organization",
		version: PACKAGE_VERSION,
		$InferServerPlugin: {} as OrganizationPlugin<{
			ac: CO["ac"] extends AccessControl
				? CO["ac"]
				: AccessControl<DefaultStatements>;
			roles: CO["roles"] extends Record<string, Role>
				? CO["roles"]
				: {
						admin: Role;
						member: Role;
						owner: Role;
					};
			teams: {
				enabled: CO["teams"] extends { enabled: true } ? true : false;
			};
			schema: Schema;
			dynamicAccessControl: {
				enabled: CO["dynamicAccessControl"] extends { enabled: true }
					? true
					: false;
			};
		}>,
		getActions: ($fetch, _$store, co) => ({
			$Infer: {
				ActiveOrganization: {} as OrganizationReturn,
				Organization: {} as InferOrganization<CO>,
				Invitation: {} as InferInvitation<CO>,
				Member: {} as InferMember<CO>,
				Team: {} as InferTeam<CO>,
			},
			organization: {
				checkRolePermission: <
					R extends CO extends { roles: any }
						? keyof CO["roles"]
						: "admin" | "member" | "owner",
				>(
					data: PermissionExclusive & {
						role: R;
					},
				) => {
					const isAuthorized = clientSideHasPermission({
						role: data.role as string,
						options: {
							ac: options?.ac,
							roles: roles,
						},
						permissions: data.permissions as any,
					});
					return isAuthorized;
				},
			},
		}),
		getAtoms: ($fetch) => {
			const listOrganizations = useAuthQuery<InferOrganization<CO>[]>(
				$listOrg,
				"/organization/list",
				$fetch,
				{
					method: "GET",
				},
			);
			const activeOrganization = useAuthQuery<
				Prettify<
					InferOrganization<CO> & {
						members: InferMember<CO>[];
						invitations: InferInvitation<CO>[];
					}
				>
			>(
				[$activeOrgSignal],
				"/organization/get-full-organization",
				$fetch,
				() => ({
					method: "GET",
				}),
			);

			const activeMember = useAuthQuery<Member>(
				[$activeOrgSignal, $activeMemberSignal],
				"/organization/get-active-member",
				$fetch,
				{
					method: "GET",
				},
			);

			const activeMemberRole = useAuthQuery<{ role: string }>(
				[$activeOrgSignal, $activeMemberRoleSignal],
				"/organization/get-active-member-role",
				$fetch,
				{
					method: "GET",
				},
			);

			return {
				$listOrg,
				$activeOrgSignal,
				$activeMemberSignal,
				$activeMemberRoleSignal,
				activeOrganization,
				listOrganizations,
				activeMember,
				activeMemberRole,
			};
		},
		pathMethods: {
			"/organization/get-full-organization": "GET",
			"/organization/list-user-teams": "GET",
		},
		atomListeners: [
			{
				matcher(path) {
					return (
						path === "/organization/create" ||
						path === "/organization/delete" ||
						path === "/organization/update"
					);
				},
				signal: "$listOrg",
			},
			{
				matcher(path) {
					return path.startsWith("/organization");
				},
				signal: "$activeOrgSignal",
			},
			{
				matcher(path) {
					return (
						path.startsWith("/organization/set-active") ||
						path === "/organization/create" ||
						path === "/organization/delete" ||
						path === "/organization/remove-member" ||
						path === "/organization/leave" ||
						path === "/organization/accept-invitation"
					);
				},
				signal: "$sessionSignal",
			},
			{
				matcher(path) {
					return (
						path.includes("/organization/update-member-role") ||
						path.startsWith("/organization/set-active")
					);
				},
				signal: "$activeMemberSignal",
			},
			{
				matcher(path) {
					return (
						path.includes("/organization/update-member-role") ||
						path.startsWith("/organization/set-active")
					);
				},
				signal: "$activeMemberRoleSignal",
			},
		],
		$ERROR_CODES: ORGANIZATION_ERROR_CODES,
	} satisfies BetterAuthClientPlugin;
};

export const inferOrgAdditionalFields = <
	O extends {
		options: BetterAuthOptions;
	},
	S extends OrganizationOptions["schema"] = undefined,
>(
	schema?: S | undefined,
) => {
	type FindById<
		T extends readonly BetterAuthPlugin[],
		TargetId extends string,
	> = Extract<T[number], { id: TargetId }>;

	type Auth = O extends { options: any } ? O : { options: { plugins: [] } };

	type OrganizationPlugin = FindById<
		// @ts-expect-error
		Auth["options"]["plugins"],
		"organization"
	>;

	// The server schema can contain more properties other than additionalFields, but the client only supports additionalFields
	// if we don't remove all other properties we may see assignability issues

	type ExtractClientOnlyFields<T> = {
		[K in keyof T as T[K] extends { additionalFields: unknown }
			? K
			: never]: T[K];
	};

	type Schema = O extends Object
		? O extends Exclude<OrganizationOptions["schema"], undefined>
			? O
			: OrganizationPlugin extends { options: { schema: infer S } }
				? S extends OrganizationOptions["schema"]
					? ExtractClientOnlyFields<S>
					: undefined
				: undefined
		: undefined;
	return {} as undefined extends S ? Schema : S;
};

export type * from "./schema";
