/*
This file is part of »zeitbild«.

Copyright 2025 'kcf' <fenris@folksprak.org>

»zeitbild« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

»zeitbild« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
 */


namespace _zeitbild.service.calendar
{
	
	/**
	 */
	async function get_access_level(
		calendar_object : _zeitbild.type_calendar_object,
		user_id : (null | _zeitbild.type_user_id)
	)
	: Promise<_zeitbild.enum_access_level>
	{
		return _zeitbild.access_level_determine(
			calendar_object,
			(
				(user_id === null)
				?
				null
				:
				{
					"id": user_id,
					"object": (await _zeitbild.service.user.get(user_id)),
				}
			)
		);
	}
	
	
	/**
	 * checks if a user has a sufficient access level
	 */
	async function wrap_check_access_level<type_result>(
		calendar_object : _zeitbild.type_calendar_object,
		user_id : (null | _zeitbild.type_user_id),
		threshold : _zeitbild.enum_access_level,
		success_handler : (
			(access_level : _zeitbild.enum_access_level)
			=>
			Promise<type_result>
		)
	)
	: Promise<type_result>
	{
		const access_level : _zeitbild.enum_access_level = await get_access_level(
			calendar_object,
			user_id
		);
		if (! _zeitbild.access_level_order(threshold, access_level))
		{
			return Promise.reject<type_result>(
				new Error(
					lib_plankton.string.coin(
						"insufficient access level; at least required: {{threshold}}, actual: {{actual}}",
						{
							"threshold": _zeitbild.access_level_to_string(threshold),
							"actual": _zeitbild.access_level_to_string(access_level),
						}
					)
				)
			);
		}
		else
		{
			return success_handler(access_level);
		}
	}
	
	
	/**
	 */
	export function overview(
		user_id : (null | _zeitbild.type_user_id)
	) : Promise<
		Array<
			{
				id : _zeitbild.type_calendar_id;
				name : string;
				access_level : _zeitbild.enum_access_level;
				hue : float;
			}
		>
	>
	{
		return _zeitbild.repository.calendar.overview(user_id);
	}
	
		
	/**
	 */
	export async function get(
		calendar_id : _zeitbild.type_calendar_id,
		user_id : _zeitbild.type_user_id
	) : Promise<_zeitbild.type_calendar_object>
	{
		const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
		return wrap_check_access_level<_zeitbild.type_calendar_object>(
			calendar_object,
			user_id,
			_zeitbild.enum_access_level.view,
			() => Promise.resolve(calendar_object)
		);
	}
	
	
	/**
	 */
	export function add(
		calendar_object : _zeitbild.type_calendar_object
	) : Promise<_zeitbild.type_calendar_id>
	{
		return _zeitbild.repository.calendar.create(calendar_object);
	}
	
	
	/**
	 */
	export async function change(
		calendar_id : _zeitbild.type_calendar_id,
		calendar_object : _zeitbild.type_calendar_object,
		user_id : _zeitbild.type_user_id
	) : Promise<void>
	{
		const calendar_object_current : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
		return wrap_check_access_level<void>(
			calendar_object_current,
			user_id,
			_zeitbild.enum_access_level.admin,
			() => _zeitbild.repository.calendar.update(calendar_id, calendar_object)
		);
	}
	
	
	/**
	 */
	export async function remove(
		calendar_id : _zeitbild.type_calendar_id,
		user_id : _zeitbild.type_user_id
	) : Promise<void>
	{
		const calendar_object_current : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
		return wrap_check_access_level<void>(
			calendar_object_current,
			user_id,
			_zeitbild.enum_access_level.admin,
			() => _zeitbild.repository.calendar.delete_(calendar_id)
		);
	}
	
	
	/**
	 */
	export async function event_get(
		calendar_id : _zeitbild.type_calendar_id,
		local_resource_event_id : _zeitbild.type_local_resource_event_id,
		user_id : _zeitbild.type_user_id
	) : Promise<_zeitbild.type_event_object>
	{
		const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
			calendar_id
		);
		return wrap_check_access_level<_zeitbild.type_event_object>(
			calendar_object,
			user_id,
			_zeitbild.enum_access_level.view,
			async () => {
				const event_object : _zeitbild.type_event_object = await _zeitbild.service.resource.event_get(
					calendar_object.resource_id,
					local_resource_event_id
				);
				return Promise.resolve<_zeitbild.type_event_object>(event_object);
			}
		);
	}
	
	
	/**
	 */
	export async function event_add(
		calendar_id : _zeitbild.type_calendar_id,
		event_object : _zeitbild.type_event_object,
		user_id : _zeitbild.type_user_id
	) : Promise<
		{
			local_resource_event_id : (null | type_local_resource_event_id);
			hash : type_event_hash;
		}
	>
	{
		const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
			calendar_id
		);
		return wrap_check_access_level(
			calendar_object,
			user_id,
			_zeitbild.enum_access_level.edit,
			async () => {
				const local_resource_event_id : _zeitbild.type_local_resource_event_id = await _zeitbild.service.resource.event_add(
					calendar_object.resource_id,
					event_object
				);
				return Promise.resolve(
					{
						"local_resource_event_id": local_resource_event_id,
						"hash": get_event_hash_local(calendar_id, local_resource_event_id),
					}
				);
			}
		);
	}
	
	
	/**
	 */
	export async function event_change(
		calendar_id : _zeitbild.type_calendar_id,
		local_resource_event_id : _zeitbild.type_local_resource_event_id,
		event_object : _zeitbild.type_event_object,
		user_id : _zeitbild.type_user_id
	) : Promise<void>
	{
		const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
			calendar_id
		);
		return wrap_check_access_level<void>(
			calendar_object,
			user_id,
			_zeitbild.enum_access_level.edit,
			async () => {
				await _zeitbild.service.resource.event_change(
					calendar_object.resource_id,
					local_resource_event_id,
					event_object
				);
				return Promise.resolve<void>(undefined);
			}
		);
	}
	
	
	/**
	 */
	export async function event_remove(
		calendar_id : _zeitbild.type_calendar_id,
		local_resource_event_id : _zeitbild.type_local_resource_event_id,
		user_id : _zeitbild.type_user_id
	) : Promise<void>
	{
		const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
			calendar_id
		);
		return wrap_check_access_level<void>(
			calendar_object,
			user_id,
			_zeitbild.enum_access_level.edit,
			async () => {
				await _zeitbild.service.resource.event_remove(
					calendar_object.resource_id,
					local_resource_event_id
				);
				return Promise.resolve<void>(undefined);
			}
		);
	}
	
	
	/**
	 */
	function get_event_hash_local(
		calendar_id : _zeitbild.type_calendar_id,
		local_resource_event_id : _zeitbild.type_local_resource_event_id
	) : string
	{
		return lib_plankton.string.coin(
			"{{calendar_id}}:{{event_id}}",
			{
				"calendar_id": calendar_id.toFixed(0),
				"event_id": local_resource_event_id.toFixed(0),
			}
		)
	}
	
	
	/**
	 */
	function get_event_hash_ics_feed(
		calendar_id : _zeitbild.type_calendar_id,
		event_object : _zeitbild.type_event_object
	) : string
	{
		return lib_plankton.string.coin(
			"{{calendar_id}}~{{hash}}",
			{
				"calendar_id": calendar_id.toFixed(0),
				"hash": lib_plankton.call.convey(
					event_object,
					[
						(x : any) => lib_plankton.json.encode(x),
						(x : string) => lib_plankton.base64.encode(x),
					]
				)
			}
		);
	}
	
	/**
	 * @todo optimize by reducing the number of database queries
	 */
	async function get_events(
		calendar_id : _zeitbild.type_calendar_id,
		from_pit : lib_plankton.pit.type_pit,
		to_pit : lib_plankton.pit.type_pit,
		user_id : (null | _zeitbild.type_user_id)
	) : Promise<
		Array<
			{
				id : (null | _zeitbild.type_local_resource_event_id);
				hash : _zeitbild.type_event_hash;
				object : _zeitbild.type_event_object;
			}
		>
	>
	{
		const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(calendar_id);
		return wrap_check_access_level<Array<{id : (null | _zeitbild.type_local_resource_event_id); hash : _zeitbild.type_event_hash; object : _zeitbild.type_event_object;}>>(
			calendar_object,
			user_id,
			_zeitbild.enum_access_level.view,
			async () => {
				const resource_object : _zeitbild.type_resource_object = await _zeitbild.repository.resource.read(calendar_object.resource_id);
				switch (resource_object.kind) {
					case "local": {
						return (
							Promise.all(
								resource_object.data.event_ids
								.map(
									(event_id) => (
										_zeitbild.repository.resource.local_resource_event_read(
											calendar_object.resource_id,
											event_id
										)
										.then(
											(event_object) => Promise.resolve(
												{
													"id": event_id,
													"hash": get_event_hash_local(
														calendar_id,
														event_id,
													),
													"object": event_object,
												}
											)
										)
									)
								)
							)
							.then(
								(event_entries) => Promise.resolve(
									event_entries
									.filter(
										(event_entry : {id : (null | _zeitbild.type_local_resource_event_id); object : _zeitbild.type_event_object;}) => lib_plankton.pit.is_between(
											lib_plankton.pit.from_datetime(event_entry.object.begin),
											from_pit,
											to_pit
										)
									)
								)
							)
						);
						break;
					}
					case "ics_feed": {
						// TODO readonly
						const vcalendar : lib_plankton.ical.type_vcalendar = await lib_plankton.cache.get<lib_plankton.ical.type_vcalendar>(
							_zeitbild.cache_external_resources,
							resource_object.data.url,
							_zeitbild.conf.get().external_resources.lifetime,
							async () => {
								const url : lib_plankton.url.type_url = lib_plankton.url.decode(
									resource_object.data.url
								);
								const http_request : lib_plankton.http.type_request = {
									"version": "HTTP/2",
									"scheme": ((url.scheme === "https") ? "https" : "http"),
									"host": url.host,
									"path": (url.path ?? "/"),
									"query": url.query,
									"method": lib_plankton.http.enum_method.get,
									"headers": {},
									"body": null,
								};
								const http_response : lib_plankton.http.type_response = await lib_plankton.http.call(
									http_request,
									{
									}
								);
								const ics_raw : string = (
									(http_response.body === null)
									?
									""
									:
									http_response.body.toString()
								);
								const vcalendar_list : Array<lib_plankton.ical.type_vcalendar> = lib_plankton.ical.ics_decode_multi(
									ics_raw,
									{
										"ignore_unhandled_instruction_keys": resource_object.data.from_fucked_up_wordpress,
										"from_fucked_up_wordpress": resource_object.data.from_fucked_up_wordpress,
									}
								);
								const vcalendar : lib_plankton.ical.type_vcalendar = {
									// required
									"version": vcalendar_list[0].version,
									"prodid": vcalendar_list[0].prodid,
									"vevents": vcalendar_list.map(x => x.vevents).reduce((x, y) => x.concat(y), []),
								};
								return Promise.resolve<lib_plankton.ical.type_vcalendar>(vcalendar);
							}
						);
						return Promise.resolve(
							vcalendar.vevents
							.map(
								(vevent : lib_plankton.ical.type_vevent) => (
									(vevent.dtstart !== undefined)
									?
									{
										"name": (
											(vevent.summary !== undefined)
											?
											vevent.summary
											:
											"???"
										),
										"begin": _zeitbild.helpers.icalendar_dt_to_own_datetime(vevent.dtstart),
										"end": (
											(vevent.dtend !== undefined)
											?
											_zeitbild.helpers.icalendar_dt_to_own_datetime(vevent.dtend)
											:
											null
										),
										"location": (
											(vevent.location !== undefined)
											?
											vevent.location
											:
											null
										),
										"link": (
											(vevent.url !== undefined)
											?
											vevent.url
											:
											null
										),
										"description": (
											(vevent.description !== undefined)
											?
											vevent.description
											:
											null
										),
									}
									:
									null
								)
							)
							.filter(
								(event) => (event !== null)
							)
							.map(
								(event) => ({
									"id": null,
									"hash": get_event_hash_ics_feed(calendar_id, event),
									"object": event,
								})
							)
							.filter(
								(event_entry) => lib_plankton.pit.is_between(
									lib_plankton.pit.from_datetime(event_entry.object.begin),
									from_pit,
									to_pit
								)
							)
						);
						break;
					}
					default: {
						return Promise.reject(
							new Error("invalid resource kind: " + resource_object["kind"])
						);
						break;
					}
				}
			}
		);
	}
	
	
	/**
	 */
	export async function gather_events(
		calendar_ids_wanted : (null | Array<_zeitbild.type_calendar_id>),
		from_pit : lib_plankton.pit.type_pit,
		to_pit : lib_plankton.pit.type_pit,
		user_id : (null | _zeitbild.type_user_id)
	) : Promise<Array<_zeitbild.type_event_extended>>
	{
		const calendar_ids_allowed : Array<_zeitbild.type_calendar_id> = (
			(await overview(user_id))
			.map((x : any) => x.id)
		);
		// use set intersection
		const calendar_ids : Array<_zeitbild.type_calendar_id> = (
			(
				(calendar_ids_wanted === null)
				?
				calendar_ids_allowed
				:
				(
					calendar_ids_wanted
					.filter(
						(calendar_id) => calendar_ids_allowed.includes(calendar_id)
					)
				)
			)
		);
		calendar_ids.sort();
		return lib_plankton.cache.get_complex<any, Array<_zeitbild.type_event_extended>>(
			_zeitbild.cache_regular,
			"gather_events",
			{
				"user_id": user_id,
				"from_pit": from_pit,
				"to_pit": to_pit,
				"calendar_ids": calendar_ids,
			},
			/**
			 * @todo expire?
			 */
			null,
			() => (
				Promise.all(
					calendar_ids
					.map(
						async (calendar_id) => {
							const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
								calendar_id
							);
							const access_level : _zeitbild.enum_access_level = await get_access_level(
								calendar_object,
								user_id
							);
							const events : Array<
								{
									id : (null | _zeitbild.type_local_resource_event_id);
									hash : _zeitbild.type_event_hash;
									object : _zeitbild.type_event_object;
								}
							> = await get_events(
								calendar_id,
								from_pit,
								to_pit,
								user_id
							);
							return Promise.resolve(
								events
								.map(
									(event_entry) => ({
										"hash": event_entry.hash,
										"calendar_id": calendar_id,
										"calendar_name": calendar_object.name,
										"hue": calendar_object.hue,
										"access_level": access_level,
										"event_id": event_entry.id,
										"event_object": event_entry.object,
									})
								)
							);
						}
					)
				)
				.then(
					(sub_results) => sub_results.reduce(
						(x, y) => x.concat(y),
						[]
					)
				)
			)
		);
	}

}
