import { Room, ScheduleException, ScheduleSeries, Status, User } from "entity";
import { IsNull, SelectQueryBuilder } from "typeorm";
import CommonHelper from "../helpers/common";
import { OrmHelper } from "../helpers/orm";
import dayjs from "dayjs";
import { RoomModel } from "./room";
import { filter } from "compression";

type RecurrenceInput = {
	type: "daily" | "weekly" | "monthly";
	interval?: number;
	daysOfWeek?: string[];
};

type ScheduleSeriesPayload = {
	roomId: string;
	doctorId: string;
	title: string;
	quota?: number | null;
	startDate: string;
	untilDate?: string | null;
	startTime: string;
	endTime: string;
	recurrence?: RecurrenceInput | null;
};

type ScheduleExceptionPayload = {
	occurrenceDate: string;
	isCancelled?: boolean;
	overrideRoomId?: string | null;
	overrideDoctorId?: string | null;
	overrideTitle?: string | null;
	overrideStartAt?: string | null;
	overrideEndAt?: string | null;
};

type CalendarQueryOptions = {
	limit?: number;
	page?: number;
};

export class ScheduleModel {
	static async listSeries(filterObj = {}): Promise<SelectQueryBuilder<any>> {
		const repo = OrmHelper.DB.getRepository(ScheduleSeries);
		let whereAttr: string[] = [];
		let whereVal: any = {};
		if (filterObj && Object.keys(filterObj).length > 0) {
			const filterResult = CommonHelper.handleQueryFilter(filterObj);
			whereAttr = [...whereAttr, ...filterResult.whereAttr];
			whereVal = { ...whereVal, ...filterResult.whereVal };
		}

		let query = repo
			.createQueryBuilder("ScheduleSeries")
			.leftJoinAndSelect("ScheduleSeries.doctor", "Doctor")
			.leftJoinAndSelect("ScheduleSeries.room", "Room");
		if (whereAttr.length !== 0) {
			query = query.where(whereAttr.join(" and "), whereVal);
		}
		return query;
	}

	static async getScheduleCalendarRange(start: string, end: string, filterObj: any, options: CalendarQueryOptions = {}): Promise<any> {
		const startDate = dayjs(start);
		const endDate = dayjs(end);
		if (!startDate.isValid() || !endDate.isValid()) {
			throw new Error("Invalid start or end date");
		}

		const rangeStart = startDate.startOf("day").toDate();
		const rangeEnd = endDate.endOf("day").toDate();
		if (rangeStart > rangeEnd) {
			throw new Error("Start date must be before end date");
		}

		const filterRoomId = filterObj && filterObj.roomId !== "" && filterObj.roomId !== null && filterObj.roomId !== undefined ? String(filterObj.roomId) : "";
		const filterDoctorId = filterObj && filterObj.doctorId !== "" && filterObj.doctorId !== null && filterObj.doctorId !== undefined ? String(filterObj.doctorId) : "";

		const page = options.page ?? 1;
		const limit = options.limit ?? 10;
		const offset = (page - 1) * limit;

		const query = await RoomModel.list({ ...filterObj, with_deleted: false });
		const roomQuery = query
			.select(["Room.id", "Room.room", "Room.code", "Room.picture", "Room.location", "Room.status", "Department.name"])
			.leftJoin("Room.department", "Department")
			.where("Department.name = :departmentName", { departmentName: "Outpatient" })
			.andWhere(filterRoomId !== "" ? "Room.id = :roomId" : "1=1", { roomId: filterRoomId });

		const roomTotalCount = await roomQuery
			.getCount();
		const rooms = await roomQuery
			.orderBy("Room.room", "ASC")
			.offset(offset)
			.limit(limit)
			.getMany();
		const roomIds = rooms.map((room) => room.id);

		const seriesQuery = await ScheduleModel.listSeries(filterObj);
		const seriesList = await seriesQuery
			.where("ScheduleSeries.room_id IN (:...roomIds)", { roomIds })
			.andWhere("ScheduleSeries.start_date <= :rangeEndDate", { rangeEndDate: dayjs(rangeEnd).format("YYYY-MM-DD") })
			.andWhere("(ScheduleSeries.until_date IS NULL OR ScheduleSeries.until_date >= :rangeStartDate)", { rangeStartDate: dayjs(rangeStart).format("YYYY-MM-DD") })
			.andWhere("ScheduleSeries.deleted_at IS NULL")
			.andWhere(filterDoctorId !== "" ? "ScheduleSeries.doctor_id = :doctorId" : "1=1", { doctorId: filterDoctorId })
			.orderBy("ScheduleSeries.start_time", "ASC")
			.getMany();

		const allEvents: any[] = [];
		if (seriesList.length > 0) {
			const seriesIds = seriesList.map((series) => series.id);
			const usageMap = await ScheduleModel.getDailyUsedQuotaMap(seriesIds, rangeStart, rangeEnd);

			const exceptionRepo = OrmHelper.DB.getRepository(ScheduleException);
			const exceptions = await exceptionRepo
				.createQueryBuilder("ScheduleException")
				.leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom")
				.leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor")
				.where("ScheduleException.series_id IN (:...seriesIds)", { seriesIds })
				.andWhere("ScheduleException.occurrence_date BETWEEN :rangeStart AND :rangeEnd", {
					rangeStart: ScheduleModel.formatYmd(rangeStart),
					rangeEnd: ScheduleModel.formatYmd(rangeEnd),
				})
				.andWhere("ScheduleException.deleted_at IS NULL")
				.getMany();

			const exceptionMap = new Map<string, ScheduleException>();
			for (const ex of exceptions) {
				const key = `${ex.series_id}:${dayjs(ex.occurrence_date).format("YYYY-MM-DD")}`;
				exceptionMap.set(key, ex);
			}

			for (const series of seriesList) {
				const occurrenceDates = ScheduleModel.expandSeriesOccurrences(series, rangeStart, rangeEnd);
				for (const occurrenceDate of occurrenceDates) {
					const occurrenceYmd = dayjs(occurrenceDate).format("YYYY-MM-DD");
					const exception = exceptionMap.get(`${series.id}:${occurrenceYmd}`);
					if (exception && exception.is_cancelled) continue;

					const defaultStart = ScheduleModel.combineDateAndTime(occurrenceYmd, series.start_time);
					const defaultEnd = ScheduleModel.combineDateAndTime(occurrenceYmd, series.end_time);

					const doctor = exception?.override_doctor ?? series.doctor;
					const room = exception?.override_room ?? series.room;
					const eventStart = exception?.override_start_at ? exception.override_start_at : defaultStart;
					const eventEnd = exception?.override_end_at ? exception.override_end_at : defaultEnd;
					const eventTitle = exception?.override_title || series.title;

					const roomId = room?.id || null;
					const doctorId = doctor?.id || null;
					const roomName = (room as any)?.room || (room as any)?.name || (room as any)?.code || "";
					const doctorName = (doctor as any)?.name || (doctor as any)?.fullname || (doctor as any)?.username || "";

					allEvents.push({
						id: `occ:${series.id}:${occurrenceYmd}`,
						series_id: series.id,
						title: eventTitle,
						room_id: roomId,
						room_name: roomName,
						doctor_id: doctorId,
						doctor_name: doctorName,
						start_at: eventStart,
						end_at: eventEnd,
						quota: series.quota ?? null,
						used_quota: usageMap.get(`${series.id}:${occurrenceYmd}`) || 0,
						left_quota:
							typeof series.quota === "number"
								? Math.max(0, series.quota - (usageMap.get(`${series.id}:${occurrenceYmd}`) || 0))
								: null,
						is_recurring: !!series.recurrence_rule,
						is_exception: !!exception,
						status: "confirmed",
					});
				}
			}
		}

		for (const room of rooms) {
			const roomEvents = allEvents.filter((event) => event.room_id === room.id);
			room.events = roomEvents;
		}

		return {
			count: rooms.length,
			page: page,
			total_count: roomTotalCount,
			list: rooms,
		};
	}

	static async getScheduleOptions(filterObj = {}): Promise<any> {
		const query = await ScheduleModel.listSeries(filterObj);
		query.andWhere("ScheduleSeries.deleted_at IS NULL");
		const seriesList: ScheduleSeries[] = await query.getMany();

		const doctorsMap = new Map<string, any>();
		const roomsMap = new Map<string, any>();

		for (const series of seriesList) {
			if (series.doctor?.id) {
				doctorsMap.set(series.doctor.id, {
					id: series.doctor.id,
					title: (series.doctor as any).name || (series.doctor as any).username || "Doctor",
				});
			}
			if (series.room?.id) {
				roomsMap.set(series.room.id, {
					id: series.room.id,
					title: (series.room as any).room || (series.room as any).code || "Room",
				});
			}
		}

		return {
			doctors: [...doctorsMap.values()],
			rooms: [...roomsMap.values()],
		};
	}

	static async getSeriesById(id: string): Promise<any> {
		const series = await OrmHelper.DB.getRepository(ScheduleSeries)
			.createQueryBuilder("ScheduleSeries")
			.leftJoinAndSelect("ScheduleSeries.doctor", "Doctor")
			.leftJoinAndSelect("ScheduleSeries.room", "Room")
			.where("ScheduleSeries.id = :id", { id })
			.andWhere("ScheduleSeries.deleted_at IS NULL")
			.getOne();

		if (!series) throw new Error("Schedule series not found");

		const exceptions = await OrmHelper.DB.getRepository(ScheduleException)
			.createQueryBuilder("ScheduleException")
			.leftJoinAndSelect("ScheduleException.override_room", "OverrideRoom")
			.leftJoinAndSelect("ScheduleException.override_doctor", "OverrideDoctor")
			.where("ScheduleException.series_id = :id", { id })
			.andWhere("ScheduleException.deleted_at IS NULL")
			.orderBy("ScheduleException.occurrence_date", "ASC")
			.getMany();

		const recurrenceRule = series.recurrence_rule;
		const { recurrence_rule: _omitRule, ...seriesRest } = series as ScheduleSeries & { recurrence_rule?: string | null };

		return {
			series: {
				...seriesRest,
				recurrence: ScheduleModel.rRuleToRecurrence(recurrenceRule),
			},
			exceptions,
		};
	}

	static async createSeries(payload: ScheduleSeriesPayload, actor: string): Promise<ScheduleSeries> {
		const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } });
		if (!room) throw new Error("Room not found");

		const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } });
		if (!doctor) throw new Error("Doctor not found");

		const series = new ScheduleSeries();
		series.room_id = payload.roomId;
		series.doctor_id = payload.doctorId;
		series.room = room;
		series.doctor = doctor;
		series.title = payload.title;
		series.quota = payload.quota ?? null;
		series.start_date = new Date(payload.startDate);
		series.until_date = payload.untilDate ? new Date(payload.untilDate) : null;
		series.start_time = payload.startTime;
		series.end_time = payload.endTime;
		series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null);
		series.status = Status.Active;
		series.created_by = actor;
		series.updated_by = actor;

		return OrmHelper.DB.manager.save(series);
	}

	static async updateSeries(id: string, payload: ScheduleSeriesPayload, actor: string): Promise<ScheduleSeries> {
		const series = await OrmHelper.DB.getRepository(ScheduleSeries)
			.createQueryBuilder("ScheduleSeries")
			.where("ScheduleSeries.id = :id", { id })
			.andWhere("ScheduleSeries.deleted_at IS NULL")
			.getOne();
		if (!series) throw new Error("Schedule series not found");

		const room = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.roomId } });
		if (!room) throw new Error("Room not found");

		const doctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.doctorId } });
		if (!doctor) throw new Error("Doctor not found");

		series.room_id = payload.roomId;
		series.doctor_id = payload.doctorId;
		series.room = room;
		series.doctor = doctor;
		series.title = payload.title;
		series.quota = payload.quota ?? null;
		series.start_date = new Date(payload.startDate);
		series.until_date = payload.untilDate ? new Date(payload.untilDate) : null;
		series.start_time = payload.startTime;
		series.end_time = payload.endTime;
		series.recurrence_rule = ScheduleModel.recurrenceToRRule(payload.recurrence || null);
		series.updated_by = actor;

		return OrmHelper.DB.manager.save(series);
	}

	static async createSeriesException(seriesId: string, payload: ScheduleExceptionPayload, actor: string): Promise<ScheduleException> {
		const series = await OrmHelper.DB.getRepository(ScheduleSeries).findOne({ where: { id: seriesId } });
		if (!series) throw new Error("Schedule series not found");

		let overrideRoom: Room = null;
		if (payload.overrideRoomId) {
			overrideRoom = await OrmHelper.DB.getRepository(Room).findOne({ where: { id: payload.overrideRoomId } });
			if (!overrideRoom) throw new Error("Override room not found");
		}

		let overrideDoctor: User = null;
		if (payload.overrideDoctorId) {
			overrideDoctor = await OrmHelper.DB.getRepository(User).findOne({ where: { id: payload.overrideDoctorId } });
			if (!overrideDoctor) throw new Error("Override doctor not found");
		}

		const occurrenceDate = new Date(payload.occurrenceDate);
		const occurrenceYmd = ScheduleModel.formatYmd(occurrenceDate);

		let exception = await OrmHelper.DB.getRepository(ScheduleException)
			.createQueryBuilder("ScheduleException")
			.where("ScheduleException.series_id = :seriesId", { seriesId })
			.andWhere("ScheduleException.occurrence_date = :occurrenceDate", { occurrenceDate: occurrenceYmd })
			.andWhere("ScheduleException.deleted_at IS NULL")
			.getOne();

		if (!exception) {
			exception = new ScheduleException();
			exception.series_id = seriesId;
			exception.series = series;
			exception.occurrence_date = occurrenceDate;
			exception.created_by = actor;
		}

		exception.is_cancelled = !!payload.isCancelled;
		exception.override_room_id = payload.overrideRoomId || null;
		exception.override_doctor_id = payload.overrideDoctorId || null;
		exception.override_room = overrideRoom;
		exception.override_doctor = overrideDoctor;
		exception.override_title = payload.overrideTitle || null;
		exception.override_start_at = payload.overrideStartAt ? new Date(payload.overrideStartAt) : null;
		exception.override_end_at = payload.overrideEndAt ? new Date(payload.overrideEndAt) : null;
		exception.updated_by = actor;

		return OrmHelper.DB.manager.save(exception);
	}

	static async deleteSeries(id: string, hard: boolean, actor: string): Promise<number> {
		const manager = OrmHelper.DB.manager;

		if (hard) {
			await manager.delete(ScheduleException, { series_id: id });
			const result = await manager.delete(ScheduleSeries, { id });
			return result.affected ?? 0;
		}

		const series = await OrmHelper.DB.getRepository(ScheduleSeries)
			.createQueryBuilder("ScheduleSeries")
			.where("ScheduleSeries.id = :id", { id })
			.andWhere("ScheduleSeries.deleted_at IS NULL")
			.getOne();

		if (!series) return 0;

		const now = new Date();

		await manager.transaction(async (tx) => {
			await tx
				.createQueryBuilder()
				.update(ScheduleException)
				.set({ deleted_at: now, deleted_by: actor })
				.where("series_id = :id", { id })
				.andWhere("deleted_at IS NULL")
				.execute();

			series.deleted_at = now;
			series.deleted_by = actor;
			await tx.save(series);
		});

		return 1;
	}

	private static expandSeriesOccurrences(series: ScheduleSeries, rangeStart: Date, rangeEnd: Date): Date[] {
		const seriesStart = ScheduleModel.startOfDay(new Date(series.start_date));
		const seriesUntil = series.until_date ? ScheduleModel.endOfDay(new Date(series.until_date)) : null;
		const rule = ScheduleModel.parseRRule(series.recurrence_rule || "");
		const ruleUntil = rule.until ? ScheduleModel.endOfDay(rule.until) : null;

		const effectiveStart = new Date(Math.max(rangeStart.getTime(), seriesStart.getTime()));
		let effectiveEnd = new Date(rangeEnd);
		if (seriesUntil && seriesUntil < effectiveEnd) effectiveEnd = seriesUntil;
		if (ruleUntil && ruleUntil < effectiveEnd) effectiveEnd = ruleUntil;
		if (effectiveStart > effectiveEnd) return [];

		if (!rule.freq) {
			return seriesStart >= effectiveStart && seriesStart <= effectiveEnd ? [seriesStart] : [];
		}

		const dates: Date[] = [];
		let generatedCount = 0;
		let current = new Date(seriesStart);
		const byDay = rule.byDay?.length ? rule.byDay : [ScheduleModel.weekdayToRRule(seriesStart.getDay())];
		const interval = rule.interval || 1;

		while (current <= effectiveEnd) {
			if (ScheduleModel.matchesRule(current, seriesStart, rule.freq, interval, byDay)) {
				generatedCount += 1;
				if (current >= effectiveStart) {
					dates.push(new Date(current));
				}
				if (rule.count && generatedCount >= rule.count) break;
			}
			current.setDate(current.getDate() + 1);
		}

		return dates;
	}

	private static matchesRule(date: Date, start: Date, freq: string, interval: number, byDay: string[]): boolean {
		const diffDays = Math.floor((ScheduleModel.startOfDay(date).getTime() - ScheduleModel.startOfDay(start).getTime()) / 86400000);
		if (diffDays < 0) return false;

		if (freq === "DAILY") return diffDays % interval === 0;
		if (freq === "WEEKLY") {
			const weekIndex = Math.floor(diffDays / 7);
			return weekIndex % interval === 0 && byDay.includes(ScheduleModel.weekdayToRRule(date.getDay()));
		}
		if (freq === "MONTHLY") {
			const months = (date.getFullYear() - start.getFullYear()) * 12 + (date.getMonth() - start.getMonth());
			return months % interval === 0 && date.getDate() === start.getDate();
		}
		return false;
	}

	private static parseRRule(rule: string): { freq?: string; interval?: number; count?: number; until?: Date; byDay?: string[] } {
		if (!rule) return {};
		const pairs = rule.split(";").map((x) => x.trim()).filter(Boolean);
		const out: { [key: string]: string } = {};
		for (const p of pairs) {
			const [k, v] = p.split("=");
			if (k && v) out[k.toUpperCase()] = v;
		}

		const untilRaw = out.UNTIL;
		let untilDate: Date = null;
		if (untilRaw) {
			if (/^\d{8}$/.test(untilRaw)) {
				untilDate = new Date(`${untilRaw.slice(0, 4)}-${untilRaw.slice(4, 6)}-${untilRaw.slice(6, 8)}T00:00:00`);
			} else if (/^\d{8}T\d{6}Z$/.test(untilRaw)) {
				untilDate = new Date(`${untilRaw.slice(0, 4)}-${untilRaw.slice(4, 6)}-${untilRaw.slice(6, 8)}T${untilRaw.slice(9, 11)}:${untilRaw.slice(11, 13)}:${untilRaw.slice(13, 15)}Z`);
			} else {
				untilDate = new Date(untilRaw);
			}
		}

		return {
			freq: out.FREQ?.toUpperCase(),
			interval: out.INTERVAL ? Number(out.INTERVAL) : 1,
			count: out.COUNT ? Number(out.COUNT) : undefined,
			until: untilDate && !isNaN(untilDate.getTime()) ? untilDate : undefined,
			byDay: out.BYDAY ? out.BYDAY.split(",").map((x) => x.trim().toUpperCase()) : undefined,
		};
	}

	private static combineDateAndTime(ymd: string, hhmmss: string) {
		return `${ymd} ${hhmmss}`;
	}

	private static startOfDay(date: Date): Date {
		return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
	}

	private static endOfDay(date: Date): Date {
		return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
	}

	private static formatYmd(date: Date): string {
		const y = date.getFullYear();
		const m = String(date.getMonth() + 1).padStart(2, "0");
		const d = String(date.getDate()).padStart(2, "0");
		return `${y}-${m}-${d}`;
	}

	private static weekdayToRRule(day: number): string {
		return ["SU", "MO", "TU", "WE", "TH", "FR", "SA"][day];
	}

	private static recurrenceToRRule(recurrence: RecurrenceInput | null): string {
		if (!recurrence) return null;
		const interval = recurrence.interval && recurrence.interval > 0 ? recurrence.interval : 1;
		if (recurrence.type === "weekly") {
			const byDay = recurrence.daysOfWeek && recurrence.daysOfWeek.length > 0 ? recurrence.daysOfWeek.join(",") : "";
			return byDay ? `FREQ=WEEKLY;INTERVAL=${interval};BYDAY=${byDay}` : `FREQ=WEEKLY;INTERVAL=${interval}`;
		}
		if (recurrence.type === "daily") {
			return `FREQ=DAILY;INTERVAL=${interval}`;
		}
		if (recurrence.type === "monthly") {
			return `FREQ=MONTHLY;INTERVAL=${interval}`;
		}
		throw new Error("Unsupported recurrence type");
	}

	private static rRuleToRecurrence(recurrence_rule: string | null): RecurrenceInput | null {
		if (!recurrence_rule || !String(recurrence_rule).trim()) return null;
		const parsed = ScheduleModel.parseRRule(recurrence_rule);
		if (!parsed.freq) return null;

		const interval = parsed.interval && parsed.interval > 0 ? parsed.interval : 1;
		const freq = parsed.freq.toUpperCase();

		if (freq === "DAILY") {
			return { type: "daily", interval };
		}
		if (freq === "WEEKLY") {
			const out: RecurrenceInput = { type: "weekly", interval };
			if (parsed.byDay && parsed.byDay.length > 0) {
				out.daysOfWeek = parsed.byDay;
			}
			return out;
		}
		if (freq === "MONTHLY") {
			return { type: "monthly", interval };
		}
		return null;
	}

	private static async getDailyUsedQuotaMap(seriesIds: string[], rangeStart: Date, rangeEnd: Date): Promise<Map<string, number>> {
		const usedQuotaMap = new Map<string, number>();
		if (seriesIds.length === 0) return usedQuotaMap;

		const placeholders = seriesIds.map((_, index) => `$${index + 1}`).join(", ");
		const startParamIndex = seriesIds.length + 1;
		const endParamIndex = seriesIds.length + 2;
		const params = [...seriesIds, ScheduleModel.formatYmd(rangeStart), ScheduleModel.formatYmd(rangeEnd)];

		const query = `
			SELECT
				ro.schedule_series_id AS series_id,
				DATE(ro.visit_date) AS visit_date,
				COUNT(ro.id) AS used_count
			FROM emr.emr_registration_outpatient ro
			WHERE ro.schedule_series_id IN (${placeholders})
				AND DATE(ro.visit_date) BETWEEN $${startParamIndex} AND $${endParamIndex}
				AND ro.deleted_at IS NULL
			GROUP BY ro.schedule_series_id, DATE(ro.visit_date)
		`;

		const rows: Array<{ series_id: string; visit_date: string | Date; used_count: string | number }> = await OrmHelper.DB.manager.query(
			query,
			params
		);

		for (const row of rows) {
			const occurrenceYmd = ScheduleModel.formatYmd(new Date(row.visit_date));
			usedQuotaMap.set(`${row.series_id}:${occurrenceYmd}`, Number(row.used_count) || 0);
		}

		return usedQuotaMap;
	}
}
