import { eq, inArray } from 'astro:db';
import { type Cause, Effect } from '../../../effect.js';
import { SDKCoreError, StudioCMS_SDK_Error } from '../errors.js';
import {
	tsOAuthAccounts,
	tsPageContent,
	tsPageDataCategories,
	tsPageDataTags,
	tsPermissions,
	tsUsers,
} from '../tables.js';
import type {
	CombinedPageData,
	CombinedUserData,
	FolderNode,
	MetaOnlyPageData,
	PageDataReturnType,
	tsPageDataSelect,
	tsUsersSelect,
} from '../types/index.js';
import { AstroDB } from './db.js';
import { SDKCore_FolderTree } from './foldertree.js';
import { SDKCore_Parsers } from './parsers.js';

/**
 * Effect service providing core data collectors for StudioCMS SDK.
 *
 * @remarks
 * This service includes methods for collecting categories, tags, page data, and user data,
 * aggregating information from the database and related services. All methods return
 * `Effect.Effect` instances for composable error handling and async operations.
 *
 * @example
 * ```typescript
 * const collectors = yield* SDKCore_Collectors;
 * const categories = yield* collectors.collectCategories([1, 2, 3]);
 * ```
 *
 * @service SDKCore_Collectors
 * @dependencies AstroDB, SDKCore_FolderTree, SDKCore_Parsers
 *
 * @method collectCategories
 *   Collects categories by their IDs.
 *   @param categoryIds - Array of category IDs.
 *   @returns Effect resolving to array of categories.
 *   @throws SDKCoreError if database error occurs.
 *
 * @method collectTags
 *   Collects tags by their IDs.
 *   @param tagIds - Array of tag IDs.
 *   @returns Effect resolving to array of tags.
 *   @throws SDKCoreError if database error occurs.
 *
 * @method collectPageData
 *   Collects and combines all data related to a page, including categories, tags, contributors, author, and content.
 *   @param page - Page data object.
 *   @param tree - FolderNode tree.
 *   @param metaOnly - If true, returns only metadata.
 *   @returns Effect resolving to combined page data or meta-only data.
 *   @throws SDKCoreError if database or unknown error occurs.
 *
 * @method collectUserData
 *   Collects user data including OAuth and permissions.
 *   @param user - User data object.
 *   @returns Effect resolving to combined user data.
 *   @throws SDKCoreError if database error occurs.
 */
export class SDKCore_Collectors extends Effect.Service<SDKCore_Collectors>()('SDKCore_Collectors', {
	effect: Effect.gen(function* () {
		const dbService = yield* AstroDB;
		const folderTreeService = yield* SDKCore_FolderTree;
		const parseService = yield* SDKCore_Parsers;

		/**
		 * Collects categories based on the provided category IDs.
		 *
		 * @param categoryIds - An array of category IDs to collect.
		 * @returns A promise that resolves to an array of collected categories.
		 * @throws {StudioCMS_SDK_Error} If there is an error while collecting categories.
		 */
		const collectCategories = (categoryIds: number[]) =>
			Effect.gen(function* () {
				const categories: CombinedPageData['categories'] = [];

				if (categoryIds.length > 0) {
					const resItem = yield* dbService.execute((db) =>
						db
							.select()
							.from(tsPageDataCategories)
							.where(inArray(tsPageDataCategories.id, categoryIds))
					);

					if (resItem) categories.push(...resItem);
				}

				return categories as CombinedPageData['categories'];
			});

		/**
		 * Collects tags based on the provided tag IDs.
		 *
		 * @param tagIds - An array of tag IDs to collect.
		 * @returns A promise that resolves to an array of tags.
		 * @throws {StudioCMS_SDK_Error} If an error occurs while fetching the tags.
		 */
		const collectTags = (tagIds: number[]) =>
			Effect.gen(function* () {
				const tags: CombinedPageData['tags'] = [];

				if (tagIds.length > 0) {
					const resItem = yield* dbService.execute((db) =>
						db.select().from(tsPageDataTags).where(inArray(tsPageDataTags.id, tagIds))
					);

					if (resItem) tags.push(...resItem);
				}

				return tags as CombinedPageData['tags'];
			});

		/**
		 * Transforms the collected page data to include only metadata.
		 *
		 * @param data - The page data to transform.
		 * @returns A promise that resolves to the transformed page data.
		 * @throws {UnknownException} If an error occurs during transformation.
		 */
		const transformPageDataToMetaOnly = <T extends CombinedPageData[] | CombinedPageData>(
			data: T
		): Effect.Effect<PageDataReturnType<T>, Cause.UnknownException, never> =>
			Effect.try(() => {
				if (Array.isArray(data)) {
					return data.map(
						({ defaultContent, multiLangContent, ...rest }) => rest
					) as PageDataReturnType<T>;
				}
				const {
					defaultContent: _dump1,
					multiLangContent: _dump2,
					...rest
				} = data as CombinedPageData;
				return rest as PageDataReturnType<T>;
			});

		function collectPageData(
			page: tsPageDataSelect,
			tree: FolderNode[]
		): Effect.Effect<CombinedPageData, SDKCoreError, never>;
		function collectPageData(
			page: tsPageDataSelect,
			tree: FolderNode[],
			metaOnly: boolean
		): Effect.Effect<MetaOnlyPageData, SDKCoreError, never>;
		/**
		 * Collects and combines various data related to a page.
		 *
		 * @param page - The page data to collect additional information for.
		 * @param tree - The FolderNode tree
		 * @param metaOnly - Only return the metadata and not the pageContent
		 * @returns A promise that resolves to the combined page data.
		 * @throws {StudioCMS_SDK_Error} If an error occurs while collecting page data.
		 */
		function collectPageData(page: tsPageDataSelect, tree: FolderNode[], metaOnly = false) {
			return Effect.gen(function* () {
				const categoryIds = yield* parseService.parseIdNumberArray(page.categories || []);
				const tagIds = yield* parseService.parseIdNumberArray(page.tags || []);
				const contributorIds = yield* parseService.parseIdStringArray(page.contributorIds || []);

				const categories = yield* collectCategories(categoryIds);
				const tags = yield* collectTags(tagIds);

				const authorData = yield* dbService.execute((db) =>
					db
						.select()
						.from(tsUsers)
						.where(eq(tsUsers.id, page.authorId || ''))
						.get()
				);

				let contributorsData: tsUsersSelect[];

				if (contributorIds.length) {
					contributorsData = yield* dbService.execute((db) =>
						db.select().from(tsUsers).where(inArray(tsUsers.id, contributorIds))
					);
				} else {
					contributorsData = [];
				}

				let multiLanguageContentData:
					| {
							id: string;
							contentLang: string;
							contentId: string;
							content: string | null;
					  }[]
					| undefined;

				if (!metaOnly) {
					multiLanguageContentData = yield* dbService.execute((db) =>
						db.select().from(tsPageContent).where(eq(tsPageContent.contentId, page.id))
					);
				}

				const defaultLanguageContentData = multiLanguageContentData?.find(
					(content) => content.contentLang === page.contentLang
				);

				const safeSlug = page.slug === 'index' ? '/' : `/${page.slug}`;

				let urlRoute = safeSlug;

				if (page.parentFolder) {
					const urlParts = yield* folderTreeService.findNodesAlongPathToId(tree, page.parentFolder);
					urlRoute = `/${urlParts.map((part) => part.name).join('/')}${safeSlug}`;
				}

				const returnData = {
					...page,
					urlRoute,
					categories,
					tags,
					contributorIds,
					authorData,
					contributorsData,
					multiLangContent: multiLanguageContentData,
					defaultContent: defaultLanguageContentData,
				} as CombinedPageData;

				if (metaOnly) {
					return yield* transformPageDataToMetaOnly(returnData);
				}

				return returnData;
			}).pipe(
				Effect.catchTags({
					LibSQLClientError: (cause) =>
						Effect.fail(
							new SDKCoreError({
								type: 'LibSQLDatabaseError',
								cause: new StudioCMS_SDK_Error(`Error collecting page data: ${cause}`),
							})
						),
					UnknownException: (cause) =>
						Effect.fail(
							new SDKCoreError({
								type: 'UNKNOWN',
								cause: new StudioCMS_SDK_Error(`Error collecting page data: ${cause}`),
							})
						),
				})
			);
		}

		/**
		 * Collects user data by fetching OAuth data and permission data from the database.
		 *
		 * @param user - The user object containing user information.
		 * @returns A promise that resolves to a CombinedUserData object containing the user data, OAuth data, and permissions data.
		 * @throws {StudioCMS_SDK_Error} If an error occurs while collecting user data.
		 */
		const collectUserData = (
			user: tsUsersSelect
		): Effect.Effect<CombinedUserData, SDKCoreError, never> =>
			Effect.gen(function* () {
				const oAuthData = yield* dbService.execute((db) =>
					db.select().from(tsOAuthAccounts).where(eq(tsOAuthAccounts.userId, user.id))
				);
				const permissionsData = yield* dbService.execute((db) =>
					db.select().from(tsPermissions).where(eq(tsPermissions.user, user.id)).get()
				);

				return {
					...user,
					oAuthData,
					permissionsData,
				} as CombinedUserData;
			}).pipe(
				Effect.catchTags({
					LibSQLClientError: (cause) =>
						Effect.fail(
							new SDKCoreError({
								type: 'LibSQLDatabaseError',
								cause: new StudioCMS_SDK_Error(`Error collecting page data: ${cause}`),
							})
						),
				})
			);

		return { collectCategories, collectTags, collectPageData, collectUserData };
	}),
	dependencies: [AstroDB.Default, SDKCore_FolderTree.Default, SDKCore_Parsers.Default],
}) {}
