const dbSAPPS = require("../../helpers/db");
require("dotenv").config();
const { Op } = require("sequelize");
const axios = require("axios");
const path = require("path");
const redisClient = require(path.join(__dirname, "..", "..", "..", "..", "redisClient"));
const crypto = require("crypto");

const NATIONAL_HOLIDAY_API_URL_SETTING_KEY = "national_holiday_api_url";
const DEFAULT_NATIONAL_HOLIDAY_API_BASE_URL = "https://libur.deno.dev/api?year=";

module.exports = {
	create,
	getAll,
	getNationalHolidays,
	option,
	getHolidayListItemByDate,
	count,
	update,
	delete: _delete,
};

function normalizeCountry(input) {
	const v = String(input || "ID")
		.trim()
		.toUpperCase();
	return v || "ID";
}

function parseYear(input) {
	const y = parseInt(input, 10);
	if (!Number.isFinite(y)) return null;
	if (y < 1970 || y > 2100) return null;
	return y;
}

function toIsoDateString(value) {
	// Supports: 'YYYY-MM-DD' or other string compatible with Date
	if (!value) return null;
	if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
	const d = new Date(value);
	if (Number.isNaN(d.getTime())) return null;
	const yyyy = String(d.getFullYear());
	const mm = String(d.getMonth() + 1).padStart(2, "0");
	const dd = String(d.getDate()).padStart(2, "0");
	return `${yyyy}-${mm}-${dd}`;
}

function hashShort(input) {
	return crypto
		.createHash("sha1")
		.update(String(input || ""))
		.digest("hex")
		.slice(0, 10);
}

function sanitizeNationalHolidayApiBaseUrl(input) {
	let v = String(input || "").trim();
	if (!v) return null;

	// Remove accidental spaces/newlines
	v = v.replace(/\s+/g, "");
	if (!v) return null;
	if (v.length > 2048) return null;

	// If user omitted scheme, assume https
	if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(v)) {
		v = `https://${v}`;
	}

	// Validate URL shape without destroying the {year} placeholder.
	try {
		if (v.includes("{year}")) {
			const testUrl = v.split("{year}").join("2000");
			const parsed = new URL(testUrl);
			if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
			if (parsed.username || parsed.password) return null;
			return v;
		}

		const parsed = new URL(v);
		if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
		if (parsed.username || parsed.password) return null;
		return v;
	} catch (_e) {
		return null;
	}
}

function buildNationalHolidayApiUrl(baseUrl, year) {
	const base = String(baseUrl || "").trim();
	if (!base) return `${DEFAULT_NATIONAL_HOLIDAY_API_BASE_URL}${encodeURIComponent(year)}`;

	// If includes placeholder, replace
	if (base.includes("{year}")) {
		return base.split("{year}").join(encodeURIComponent(year));
	}

	// If already contains a year param with any value, replace that value.
	// Example: https://host/api?year=2026 -> https://host/api?year=2027
	const replaced = base.replace(/([?&]year=)([^&]*)/i, `$1${encodeURIComponent(year)}`);
	if (replaced !== base) return replaced;

	// If user put '?year' (without '=') at end, fix it.
	if (/(?:\?|&)year$/i.test(base)) {
		return `${base}=${encodeURIComponent(year)}`;
	}

	// If already has year= (typical), append year directly
	if (/year\s*=\s*$/i.test(base) || base.includes("?year=") || base.includes("&year=")) {
		return `${base}${encodeURIComponent(year)}`;
	}

	// If already has query params, append with &year=, otherwise ?year=
	return base.includes("?") ? `${base}&year=${encodeURIComponent(year)}` : `${base}?year=${encodeURIComponent(year)}`;
}

async function getCompanyNationalHolidayApiBaseUrl(companyId) {
	const cid = parseInt(companyId, 10);
	if (!Number.isFinite(cid)) return null;
	try {
		const row = await dbSAPPS.CompanySetting.findOne({
			where: { company_id: cid, key: NATIONAL_HOLIDAY_API_URL_SETTING_KEY },
			attributes: ["value"],
		});
		return sanitizeNationalHolidayApiBaseUrl(row?.value);
	} catch (_e) {
		return null;
	}
}

async function fetchNationalHolidayApi(url) {
	const res = await axios.get(url, {
		timeout: 15000,
		headers: {
			Accept: "application/json",
			"User-Agent": "SAPPS-BACKEND/holidayLists",
		},
		// Avoid throwing on non-2xx so we can return clean message
		validateStatus: () => true,
	});

	if (res.status < 200 || res.status >= 300) {
		const msg = typeof res?.data === "string" ? res.data : `Upstream responded with ${res.status}`;
		throw new Error(msg);
	}

	return res?.data;
}

/**
 * Server-side fetch national holidays to avoid browser CORS/network issues.
 * Returns normalized items: { holiday_date: 'YYYY-MM-DD', holiday_name: string, is_cuti: boolean }
 */
async function getNationalHolidays(params) {
	try {
		const country = normalizeCountry(params?.country);
		const year = parseYear(params?.year);
		const companyId = params?.company_id ?? params?.companyId ?? null;
		if (!year) {
			return { success: false, message: "Invalid year", nationalHolidays: [] };
		}

		// Currently only ID is supported by the chosen data source.
		if (country !== "ID") {
			return {
				success: false,
				message: `Unsupported country: ${country}. Only ID is supported.`,
				nationalHolidays: [],
			};
		}

		const customBaseUrl = await getCompanyNationalHolidayApiBaseUrl(companyId);
		const effectiveBaseUrl = customBaseUrl || DEFAULT_NATIONAL_HOLIDAY_API_BASE_URL;
		const urlHash = hashShort(effectiveBaseUrl);
		const cacheKey = `national-holidays:${country}:${year}:${companyId || "default"}:${urlHash}`;
		const cached = await redisClient.get(cacheKey);
		if (cached) {
			return {
				success: true,
				message: "Loaded successfully!",
				nationalHolidays: cached,
				fromCache: true,
			};
		}

		let data;
		try {
			const url = buildNationalHolidayApiUrl(effectiveBaseUrl, year);
			data = await fetchNationalHolidayApi(url);
		} catch (err) {
			// Fallback: if cache exists (race), return it; otherwise bubble clean message
			const cachedAfterError = await redisClient.get(cacheKey);
			if (cachedAfterError) {
				return {
					success: true,
					message: "Loaded from cache (upstream unavailable).",
					nationalHolidays: cachedAfterError,
					fromCache: true,
				};
			}
			return {
				success: false,
				message: err?.message || "Failed to fetch national holidays",
				nationalHolidays: [],
			};
		}

		const arr = Array.isArray(data) ? data : [];
		const normalized = arr
			.map((h) => {
				// New upstream format: { date: 'YYYY-MM-DD', name: '...' }
				const holiday_date = toIsoDateString(h?.date || h?.holiday_date || h?.tanggal);
				const holiday_name = String(h?.name || h?.holiday_name || h?.keterangan || "").trim();
				// New upstream doesn't provide joint-holiday flag; treat all as national holidays.
				const is_cuti = typeof h?.is_cuti === "boolean" ? h.is_cuti : false;
				if (!holiday_date || !holiday_name) return null;
				return { holiday_date, holiday_name, is_cuti };
			})
			.filter(Boolean);

		// Cache for 7 days (robust against upstream downtime)
		await redisClient.setEx(cacheKey, 60 * 60 * 24 * 7, normalized);

		return {
			success: true,
			message: "Loaded successfully!",
			nationalHolidays: normalized,
		};
	} catch (error) {
		return {
			success: false,
			message: error?.message || "Failed to fetch national holidays",
			nationalHolidays: [],
		};
	}
}

/**
 * Format an array of holidayLists by appending a modifier field to each company.
 */
const formatWithModifier = async (holidayLists) => {
	return Promise.all(
		holidayLists.map(async (holidayList) => {
			const modifier = await dbSAPPS.User.findOne({
				where: {
					id: holidayList.modified_by,
				},
				attributes: ["full_name"],
			});

			return {
				...holidayList,
				modifier: modifier?.full_name,
			};
		}),
	);
};

/**
 * Retrieves all holidayLists with their parent holidayList title.
 */
async function getAll(params) {
	try {
		// filter
		const filterConditions = {};

		if (params?.companiesId) {
			filterConditions.company_id = {
				[Op.in]: params?.companiesId,
			};
		}

		let holidayLists = [];
		holidayLists = await dbSAPPS.HolidayList.findAll({
			where: filterConditions,
			order: [["modified", "DESC"]],
			include: [
				{
					model: dbSAPPS.Company,
					as: "company",
					attributes: ["id", "name", "display_name"],
				},
				{
					model: dbSAPPS.HolidayListItem,
					as: "holidaylistitem",
				},
			],
		});

		holidayLists = holidayLists.map((holidayList) => {
			return {
				...holidayList.dataValues,
				company_name: holidayList?.company?.display_name,
			};
		});

		holidayLists = await formatWithModifier(holidayLists);

		return {
			holidayLists,
			success: true,
			message: "Loaded successfully!",
		};
	} catch (error) {
		const errorMessage = error?.message || "Failed to load, please try again!";
		return {
			success: false,
			message: errorMessage,
		};
	}
}

/**
 * Get holidayList items with their respective settings.
 */
async function option(params) {
	try {
		// filter
		const filterConditions = {};

		if (params.holidaylist_id) {
			filterConditions.id = {
				[Op.eq]: parseInt(params.holidaylist_id, 10),
			};
		}

		const raw = params?.companiesId;
		const companiesId = Array.isArray(raw) ? raw.map((id) => parseInt(id, 10)).filter((n) => !isNaN(n)) : [];
		if (companiesId.length > 0) {
			filterConditions.company_id = {
				[Op.in]: companiesId,
			};
		}

		let options = [];
		options = await dbSAPPS.HolidayList.findAll({
			attributes: ["id", "name", "display_name", "company_id"],
			where: filterConditions,
			order: [["name", "ASC"]],
			include: [
				{
					model: dbSAPPS.Company,
					as: "company",
					attributes: ["id", "name", "display_name"],
				},
				{
					model: dbSAPPS.HolidayListItem,
					as: "holidaylistitem",
				},
			],
		});

		options = options.map((opt) => ({
			...opt.dataValues,
			company_name: opt?.company?.display_name,
		}));

		return {
			options,
			success: true,
			message: "Loaded successfully!",
		};
	} catch (error) {
		const errorMessage = error?.message || "Failed to load, please try again!";
		return {
			success: false,
			message: errorMessage,
		};
	}
}

/**
 * Get holidayListItems by hilodayDate.
 */
async function getHolidayListItemByDate(params) {
	try {
		// filter
		const filterConditions = {};

		if (params.from_date && params.to_date) {
			// Filter holiday_date antara fromDate dan toDate (inclusive)
			filterConditions.holiday_date = {
				[Op.between]: [params.from_date, params.to_date],
			};
		}

		if (params.holiday_date || params.date) {
			filterConditions.holiday_date = {
				[Op.eq]: params.holiday_date || params.date,
			};
		}

		// Filter berdasarkan tahun jika params.year ada
		if (params.year) {
			// Tambahkan kondisi ekstrak tahun dari holiday_date sama dengan params.year
			filterConditions[Op.and] = filterConditions[Op.and] || [];
			filterConditions[Op.and].push(where(fn("YEAR", col("holiday_date")), params.year));
		}

		// Filter berdasarkan companiesId jika ada
		const raw = params?.companiesId;
		const companiesId = Array.isArray(raw) ? raw.map((id) => parseInt(id, 10)).filter((n) => !isNaN(n)) : [];
		if (companiesId.length > 0) {
			filterConditions.company_id = {
				[Op.in]: companiesId,
			};
		}

		let holidayListItems = [];
		holidayListItems = await dbSAPPS.HolidayListItem.findAll({
			attributes: [
				"id",
				"holiday_date",
				"display_name",
				"description",
				"weekly_off",
				"is_national_holiday",
				"is_joint_holiday",
				"is_weekend_holiday",
				"company_id",
				"holidaylist_id",
			],
			where: filterConditions,
			order: [["holiday_date", "ASC"]],
			include: [
				{
					model: dbSAPPS.Company,
					as: "company",
					attributes: ["id", "name", "display_name"],
				},
				{
					model: dbSAPPS.HolidayList,
					as: "holidaylist",
					attributes: ["id", "name", "display_name", "from_date", "to_date", "description", "total_holidays", "weekly_off", "company_id"],
				},
			],
		});

		holidayListItems = holidayListItems.map((holidayListItem) => {
			return {
				...holidayListItem.dataValues,
				company_name: holidayListItem?.company?.display_name,
				holidaylist_name: holidayListItem?.holidaylist?.display_name,
			};
		});

		return {
			holidayListItems,
			success: true,
			message: "Loaded successfully!",
		};
	} catch (error) {
		const errorMessage = error?.message || "Failed to load, please try again!";
		return {
			success: false,
			message: errorMessage,
		};
	}
}

/**
 * Count active holidayList items
 */

async function count(params) {
	try {
		// filter
		const filterConditions = {};

		const count = await dbSAPPS.HolidayList.count({
			where: filterConditions,
		});

		return {
			count,
			success: true,
			message: "Loaded successfully!",
		};
	} catch (error) {
		const errorMessage = error?.message || "Failed to load, please try again!";
		return {
			success: false,
			message: errorMessage,
		};
	}
}

/**
 * Creates a new holidayList item.
 */
async function create(params) {
	const transaction = await dbSAPPS.sequelize.transaction();
	try {
		// validate existing
		const existingDisplayName = await dbSAPPS.HolidayList.findOne({
			where: { display_name: params.display_name, company_id: params.company_id },
			transaction,
		});
		if (existingDisplayName) {
			return {
				success: false,
				message: `"${params.display_name}" is already taken.`,
			};
		}

		// save
		const holidayList = await dbSAPPS.HolidayList.create(params, {
			transaction,
		});

		if (Array.isArray(params.holidays) && params.holidays.length > 0) {
			// create holidays
			const holidaysData = params.holidays.map((holiday) => ({
				...holiday,
				holidaylist_id: holidayList?.id,
				company_id: params.company_id,
			}));

			// save
			await dbSAPPS.HolidayListItem.bulkCreate(holidaysData, { transaction });
		}

		await transaction.commit();
		return {
			success: true,
			message: "Saved successfully!",
		};
	} catch (error) {
		await transaction.rollback();
		const errorMessage = error?.message || "Failed to save, please try again!";
		return {
			success: false,
			message: errorMessage,
		};
	}
}

/**
 * Updates an existing holidayList item.
 */
async function update(id, params) {
	const transaction = await dbSAPPS.sequelize.transaction();
	try {
		// validate existing
		const holidayList = await dbSAPPS.HolidayList.findByPk(id);
		if (!holidayList) return { success: false, message: "ID not found!" };

		// validate existing
		const existingDisplayName = await dbSAPPS.HolidayList.findOne({
			where: {
				display_name: params.display_name,
				company_id: params.company_id,
				id: {
					[Op.ne]: id,
				},
			},
			transaction,
		});
		if (existingDisplayName) {
			return {
				success: false,
				message: `"${params.display_name}" is already taken.`,
			};
		}

		// save
		await holidayList.update(params, { transaction });

		if (Array.isArray(params.holidays) && params.holidays.length > 0) {
			await dbSAPPS.HolidayListItem.destroy({
				where: { holidaylist_id: id },
				transaction,
			});

			// create holidays
			const holidaysData = params.holidays.map((holiday) => ({
				...holiday,
				holidaylist_id: JSON.parse(id),
				company_id: holiday.company_id || params.company_id,
			}));

			// save
			await dbSAPPS.HolidayListItem.bulkCreate(holidaysData, { transaction });
		} else {
			// delete holidays
			await dbSAPPS.HolidayListItem.destroy({
				where: { holidaylist_id: id },
				transaction,
			});
		}

		await transaction.commit();
		return {
			success: true,
			message: "Saved successfully!",
		};
	} catch (error) {
		await transaction.rollback();
		const errorMessage = error?.message || "Failed to save, please try again!";
		return {
			success: false,
			message: errorMessage,
		};
	}
}

/**
 * Delete a holidayList
 */
async function _delete(id) {
	const transaction = await dbSAPPS.sequelize.transaction();
	try {
		// validate existing
		const holidayList = await dbSAPPS.HolidayList.findByPk(id);
		if (!holidayList) return { success: false, message: "ID not found!" };

		// validate used in leavePeriod
		const usedInLeavePeriod = await dbSAPPS.LeavePeriod.count({
			where: {
				holidaylist_id: id,
			},
			transaction,
		});
		if (usedInLeavePeriod > 0) {
			throw new Error(`This holiday list is being used in Leave Period, cannot be deleted!`);
		}

		// delete holidaylistitem
		await dbSAPPS.HolidayListItem.destroy({
			where: { holidaylist_id: id },
			transaction,
		});

		// delete
		await holidayList.destroy({ transaction });

		await transaction.commit();
		return {
			success: true,
			message: "Deleted successfully!",
		};
	} catch (error) {
		await transaction.rollback();
		const errorMessage = error?.message || "Failed to delete, please try again!";
		return {
			success: false,
			message: errorMessage,
		};
	}
}
