import axios from "axios";
import { addHours } from "date-fns";
import { formatInTimeZone } from "date-fns-tz";
import { eq, sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { settings } from "../db/schema/settings.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
	type SqlQuery,
	sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { createCronJob } from "../utils/croner.utils.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { getToken, odToken } from "./opendock.utils.js";

type Releases = {
	ReleaseNumber: number;
	DeliveryState: number;
	DeliveryDate: Date;
	ReleaseState: number;
	LineItemHumanReadableId: number;
	ArticleAlias: string;
	LoadingUnits: string;
	Quantity: number;
	LineItemArticleWeight: number;
	CustomerReleaseNumber: string;
	DeliveryAddressDescription: string;
	DeliveryAddressHumanReadableId: string;
	AdditionalInformation1: string;
};

// TODO: add these docs into the db
const actaulDocks = [
	{ name: "cermac", dockId: "bcb17fae-0b1a-47a7-9fbf-594c5ebccce9" },
	{ name: "matrix", dockId: "3e32cbfc-49f4-4138-b491-9d5df9c94754" },
	{ name: "gerber", dockId: "9109e789-6c15-4cd9-87cb-de1b18627b6d" },
	{ name: "rb", dockId: "6be02526-6183-4789-a73f-e0aa155e6d1e" },
	//test server dock
	{ name: "second", dockId: "e87c92bd-13b4-4f7e-bf5e-b0182884c47a" },
];
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });

// making the cron more safe when it comes to buffer stuff
let opendockSyncRunning = false;

let lastCheck = formatInTimeZone(
	new Date().toISOString(),
	timeZone,
	"yyyy-MM-dd HH:mm:ss",
);

// const lastCheck = formatInTimeZone(
// 	new Date().toISOString(),
// 	`America/New_York`, //TODO: Pull timezone from the .env last as process.env.TIME_ZONE is not working so need to figure itout
// 	"yyyy-MM-dd HH:mm:ss",
// );

//const queue: unknown[] = [];
//const isProcessing: boolean = false;

// const parseDbDate = (value: string | Date) => {
// 	if (value instanceof Date) return value;

// 	// normalize "2026-04-08 13:10:43.280" -> "2026-04-08T13:10:43.280"
// 	const normalized = value.replace(" ", "T");

// 	// interpret that wall-clock time as America/New_York
// 	return fromZonedTime(normalized, timeZone);
// };

const postRelease = async (release: Releases) => {
	log.debug({}, `Release: ${release.ReleaseNumber} is about to be validated`);
	if (!odToken.odToken) {
		log.info({}, "Getting Auth Token");
		await getToken();
	}

	if (
		new Date(odToken.tokenDate || Date.now()).getTime() <
		Date.now() - TWENTY_FOUR_HOURS
	) {
		log.info({}, "Refreshing Auth Token");
		await getToken();
	}

	// load validation checks
	const defaultDock = await db.query.settings.findFirst({
		where: (u, { eq }) => eq(u.name, "defaultLoadType"),
	});

	// check if the release has the loadtype in it
	const releaseLoadtypeCheck = (release.AdditionalInformation1 ?? "")
		.toLowerCase()
		.split(",")
		.map((x) => x.trim())
		.includes("drop");

	// allowed to schedule now, as long as we see od in here somewhere
	const releaseOkToSchedule = (release.AdditionalInformation1 ?? "")
		.toLowerCase()
		.split(",")
		.map((x) => x.trim())
		.includes("od");

	// dock was sent over
	const releaseDockInfo = actaulDocks.some((dock) =>
		(release.AdditionalInformation1 ?? "")
			.toLowerCase()
			.split(",")
			.map((x) => x.trim())
			.includes(dock.name.toLowerCase()),
	);

	const opendDockArticleCheck = await db.query.opendockArticleSetup.findFirst({
		where: (table, { and, eq }) =>
			and(
				eq(table.av, release.LineItemHumanReadableId),
				eq(table.customer, release.DeliveryAddressHumanReadableId),
			),
	});

	// selected dock
	const releaseDocks = (release.AdditionalInformation1 ?? "")
		.toLowerCase()
		.split(",")
		.map((x) => x.trim());

	const matchedDock = actaulDocks.find((dock) =>
		releaseDocks.includes(dock.name.toLowerCase()),
	);

	const setDock =
		// validate we dont have the dock in the release
		releaseDockInfo
			? matchedDock?.dockId
			: // validate we dont have the dock in the aritcle check
				(actaulDocks.find((d) => d.name === opendDockArticleCheck?.dock)
					?.dockId ?? process.env.DEFAULT_DOCK);

	// TODO: add in docks from lst db here to make it more universal for the team
	/**
	 * ReleaseState
	 * 0 = open
	 * 1 = planned
	 * 2 = CustomCanceled
	 * 4 = internally canceled
	 */

	/**
	 * DeliveryState
	 * 0 = open
	 * 1 = inprogress
	 * 2 = loading
	 * 3 = partly shipped
	 * 4 = delivered
	 */

	const newDockApt = {
		status:
			release.DeliveryState === 0 || release.DeliveryState === 1
				? "Scheduled"
				: release.DeliveryState === 2
					? "InProgress"
					: release.DeliveryState === 3 // this will consider finished and if a correction needs made to the bol we need to cancel and reactivate the order
						? "Completed"
						: release.DeliveryState === 4 && "Completed",
		userId: process.env.DEFAULT_CARRIER, // this should be the carrierid
		loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one
		// TODO: look in the remarks in the release and if its says
		dockId: setDock, // this the warehouse we want it in to start out
		refNumbers: [release.ReleaseNumber],
		//refNumber: release.ReleaseNumber,
		start: release.DeliveryDate,
		end: addHours(release.DeliveryDate, 1),
		notes: "",
		ccEmails: [""],
		muteNotifications: true,
		metadata: {
			externalValidationFailed: false,
			externalValidationErrorMessage: null,
		},
		units: null,
		customFields: [
			{
				name: "strCustomer",
				type: "str",
				label: "Customer",
				value: `${release.DeliveryAddressDescription}`,
				description: "Who is the customer ",
				placeholder: "",
				dropDownValues: [],
				minLengthOrValue: 1,
				hiddenFromCarrier: false,
				requiredForCarrier: false,
				requiredForWarehouse: false,
			},
			{
				name: "strArticle",
				type: "str",
				label: "Article",
				value: `${release.LineItemHumanReadableId} - ${release.ArticleAlias}`,
				description: "What bottle are we sending ",
				placeholder: "",
				dropDownValues: [],
				minLengthOrValue: 1,
				hiddenFromCarrier: false,
				requiredForCarrier: false,
				requiredForWarehouse: false,
			},
			{
				name: "intPallet Count",
				type: "int",
				label: "Pallet Count",
				value: parseInt(release.LoadingUnits, 10), // do we really want to update this if its partial load as it should have been the full amount?
				description: "How many pallets",
				placeholder: "22",
				dropDownValues: [],
				minLengthOrValue: 1,
				hiddenFromCarrier: false,
				requiredForCarrier: false,
				requiredForWarehouse: false,
			},
			{
				name: "strTotal Weight",
				type: "str",
				label: "Total Weight",
				value: `${(((release.Quantity * release.LineItemArticleWeight) / 1000) * 2.20462).toFixed(2)}`,
				description: "What is the total weight of the load",
				placeholder: "",
				dropDownValues: [],
				minLengthOrValue: 1,
				hiddenFromCarrier: false,
				requiredForCarrier: false,
				requiredForWarehouse: false,
			},
			{
				name: "strCustomer ReleaseNumber",
				type: "str",
				label: "Customer Release Number",
				value: `${release.CustomerReleaseNumber}`,
				description: "What is the customer release number",
				placeholder: "",
				dropDownValues: [],
				minLengthOrValue: 1,
				hiddenFromCarrier: false,
				requiredForCarrier: false,
				requiredForWarehouse: false,
			},
		],
	};

	// TODO: pull the current added releases from the db and if one matches then we want to get its id and run the update vs create
	const { data: existingApt, error: aptError } = await tryCatch(
		db
			.select()
			.from(opendockApt)
			.where(eq(opendockApt.release, release.ReleaseNumber))
			.limit(1),
	);
	if (aptError) {
		log.error({ error: aptError }, "Error getting apt data");
		// TODO: send an error email on this one as it will cause issues
		return;
	}

	const existing = existingApt[0];

	//console.log(releaseCheck);

	if (existing) {
		const id = existing.openDockAptId;

		// deal with canceled stuff as we want this gone off od
		if (release.ReleaseState === 2 || release.ReleaseState === 4) {
			// delete the order in od and change the state to canceled in lst
			try {
				const response = await axios.delete(
					`${process.env.OPENDOCK_URL}/appointment/${id}`,
					{
						headers: {
							"content-type": "application/json; charset=utf-8",
							Authorization: `Bearer ${odToken.odToken}`,
						},
					},
					// {
					// 	hardDelete: true,
					// },
				);

				if (response.status === 400) {
					log.error({}, response.data.data.message);
					return;
				}

				// update the release in the db leaving as insert just incase something weird happened
				try {
					await db
						.update(opendockApt)
						.set({
							status: "canceled",
							upd_date: sql`Now()`,
						})
						.where(eq(opendockApt.release, release.ReleaseNumber))
						.returning();

					log.info({}, `${release.ReleaseNumber} was canceled`);
				} catch (e) {
					log.error(
						{ stack: e },
						`Error canceling the release: ${release.ReleaseNumber}`,
					);
				}
				// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
			} catch (e: any) {
				//console.info(newDockApt);
				log.error(
					{ stack: e.response.data },
					`An error has occurred during canceling of the release: ${release.ReleaseNumber}`,
				);

				return;
			}

			return;
		}

		if (
			(releaseLoadtypeCheck ||
				opendDockArticleCheck?.loadType === "drop" ||
				defaultDock?.value === "drop") &&
			release.DeliveryState === 2
		) {
			const setArrival = { ...newDockApt, status: "Arrived" };

			// set to arrived
			try {
				const response = await axios.patch(
					`${process.env.OPENDOCK_URL}/appointment/${id}`,
					setArrival,
					{
						headers: {
							"content-type": "application/json; charset=utf-8",
							Authorization: `Bearer ${odToken.odToken}`,
						},
					},
				);

				if (response.status === 400) {
					log.error({}, response.data.data.message);
					return;
				}

				// update the release in the db leaving as insert just incase something weird happened
				try {
					await db
						.insert(opendockApt)
						.values({
							release: release.ReleaseNumber,
							openDockAptId: response.data.data.id,
							appointment: response.data.data,
						})
						.onConflictDoUpdate({
							target: opendockApt.release,
							set: {
								openDockAptId: response.data.data.id,
								appointment: response.data.data,
								upd_date: sql`NOW()`,
							},
						})
						.returning();

					log.info({}, `${release.ReleaseNumber} was updated`);
				} catch (e) {
					log.error(
						{ stack: e },
						`Error updating the release: ${release.ReleaseNumber}`,
					);
				}
				// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
			} catch (e: any) {
				//console.info(newDockApt);
				log.error(
					{ stack: e.response.data },
					`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
				);

				return;
			}

			// set to inprogress
			await delay(1500);
			try {
				const response = await axios.patch(
					`${process.env.OPENDOCK_URL}/appointment/${id}`,
					newDockApt,
					{
						headers: {
							"content-type": "application/json; charset=utf-8",
							Authorization: `Bearer ${odToken.odToken}`,
						},
					},
				);

				if (response.status === 400) {
					log.error({}, response.data.data.message);
					return;
				}

				// update the release in the db leaving as insert just incase something weird happened
				try {
					await db
						.insert(opendockApt)
						.values({
							release: release.ReleaseNumber,
							openDockAptId: response.data.data.id,
							appointment: response.data.data,
						})
						.onConflictDoUpdate({
							target: opendockApt.release,
							set: {
								openDockAptId: response.data.data.id,
								appointment: response.data.data,
								upd_date: sql`NOW()`,
							},
						})
						.returning();

					log.info({}, `${release.ReleaseNumber} was updated`);
				} catch (e) {
					log.error(
						{ stack: e },
						`Error updating the release: ${release.ReleaseNumber}`,
					);
				}
				// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
			} catch (e: any) {
				//console.info(newDockApt);
				log.error(
					{ stack: e.response.data },
					`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
				);

				return;
			}
			// changing to only trigger the change if the state is 2 meaning it has a scan to it and already in progress of being loaded.
		} else if (
			release.DeliveryState === 0 ||
			release.DeliveryState === 1 ||
			release.DeliveryState === 2
		) {
			try {
				const response = await axios.patch(
					`${process.env.OPENDOCK_URL}/appointment/${id}`,
					newDockApt,
					{
						headers: {
							"content-type": "application/json; charset=utf-8",
							Authorization: `Bearer ${odToken.odToken}`,
						},
					},
				);

				if (response.status === 400) {
					log.error({}, response.data.data.message);
					return;
				}

				// update the release in the db leaving as insert just incase something weird happened
				try {
					await db
						.insert(opendockApt)
						.values({
							release: release.ReleaseNumber,
							openDockAptId: response.data.data.id,
							appointment: response.data.data,
						})
						.onConflictDoUpdate({
							target: opendockApt.release,
							set: {
								openDockAptId: response.data.data.id,
								appointment: response.data.data,
								upd_date: sql`NOW()`,
							},
						})
						.returning();

					log.info({}, `${release.ReleaseNumber} was updated`);
				} catch (e) {
					log.error(
						{ stack: e },
						`Error updating the release: ${release.ReleaseNumber}`,
					);
				}
				// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
			} catch (e: any) {
				//console.info(newDockApt);
				log.error(
					{ stack: e.response.data },
					`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
				);

				return;
			}
			// if we are finished we need to set to completed
		} else if (release.DeliveryState === 3 || release.DeliveryState === 4) {
			try {
				const response = await axios.patch(
					`${process.env.OPENDOCK_URL}/appointment/${id}`,
					newDockApt,
					{
						headers: {
							"content-type": "application/json; charset=utf-8",
							Authorization: `Bearer ${odToken.odToken}`,
						},
					},
				);

				if (response.status === 400) {
					log.error({}, response.data.data.message);
					return;
				}

				// update the release in the db leaving as insert just incase something weird happened
				try {
					await db
						.insert(opendockApt)
						.values({
							release: release.ReleaseNumber,
							openDockAptId: response.data.data.id,
							appointment: response.data.data,
						})
						.onConflictDoUpdate({
							target: opendockApt.release,
							set: {
								openDockAptId: response.data.data.id,
								appointment: response.data.data,
								status: "completed",
								upd_date: sql`NOW()`,
							},
						})
						.returning();

					log.info({}, `${release.ReleaseNumber} was updated`);
				} catch (e) {
					log.error(
						{ stack: e },
						`Error updating the release: ${release.ReleaseNumber}`,
					);
				}
				// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
			} catch (e: any) {
				//console.info(newDockApt);
				log.error(
					{ stack: e.response.data },
					`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
				);

				return;
			}
		}
	} else if (opendDockArticleCheck?.loadType === "live") {
		try {
			const response = await axios.post(
				`${process.env.OPENDOCK_URL}/appointment`,
				newDockApt,
				{
					headers: {
						"content-type": "application/json; charset=utf-8",
						Authorization: `Bearer ${odToken.odToken}`,
					},
				},
			);

			// we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
			// this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.

			if (response.status === 400) {
				log.error({}, response.data.data.message);
				return;
			}

			// the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
			// TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
			//console.info(response.data.data, "Was Created");
			try {
				await db
					.insert(opendockApt)
					.values({
						release: release.ReleaseNumber,
						openDockAptId: response.data.data.id,
						appointment: response.data.data,
					})
					.onConflictDoUpdate({
						target: opendockApt.release,
						set: {
							openDockAptId: response.data.data.id,
							appointment: response.data.data,
							upd_date: sql`NOW()`,
						},
					})
					.returning();

				log.info({}, `${release.ReleaseNumber} was created`);
			} catch (e) {
				log.error({ stack: e }, "Error creating new release");
			}
			// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
		} catch (e: any) {
			log.error(
				{ stack: e?.response?.data },
				`Error posting new release to opendock, ${release.ReleaseNumber}`,
			);

			return;
		}
	} else if (
		(releaseLoadtypeCheck ||
			opendDockArticleCheck?.loadType === "drop" ||
			defaultDock?.value === "drop") &&
		releaseOkToSchedule
	) {
		try {
			const response = await axios.post(
				`${process.env.OPENDOCK_URL}/appointment`,
				newDockApt,
				{
					headers: {
						"content-type": "application/json; charset=utf-8",
						Authorization: `Bearer ${odToken.odToken}`,
					},
				},
			);

			// we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
			// this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.

			if (response.status === 400) {
				log.error({}, response.data.data.message);
				return;
			}

			// the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
			// TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
			//console.info(response.data.data, "Was Created");
			try {
				await db
					.insert(opendockApt)
					.values({
						release: release.ReleaseNumber,
						openDockAptId: response.data.data.id,
						appointment: response.data.data,
					})
					.onConflictDoUpdate({
						target: opendockApt.release,
						set: {
							openDockAptId: response.data.data.id,
							appointment: response.data.data,
							upd_date: sql`NOW()`,
						},
					})
					.returning();

				log.info({}, `${release.ReleaseNumber} was created`);
			} catch (e) {
				log.error({ stack: e }, "Error creating new release");
			}
			// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
		} catch (e: any) {
			log.error(
				{ stack: e?.response?.data },
				`Error posting new release to opendock, ${release.ReleaseNumber}`,
			);

			return;
		}
	} else {
		// try {
		// 	const response = await axios.post(
		// 		`${process.env.OPENDOCK_URL}/appointment`,
		// 		newDockApt,
		// 		{
		// 			headers: {
		// 				"content-type": "application/json; charset=utf-8",
		// 				Authorization: `Bearer ${odToken.odToken}`,
		// 			},
		// 		},
		// 	);

		// 	// we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
		// 	// this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.

		// 	if (response.status === 400) {
		// 		log.error({}, response.data.data.message);
		// 		return;
		// 	}

		// 	// the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
		// 	// TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
		// 	//console.info(response.data.data, "Was Created");
		// 	try {
		// 		await db
		// 			.insert(opendockApt)
		// 			.values({
		// 				release: release.ReleaseNumber,
		// 				openDockAptId: response.data.data.id,
		// 				appointment: response.data.data,
		// 			})
		// 			.onConflictDoUpdate({
		// 				target: opendockApt.release,
		// 				set: {
		// 					openDockAptId: response.data.data.id,
		// 					appointment: response.data.data,
		// 					upd_date: sql`NOW()`,
		// 				},
		// 			})
		// 			.returning();

		// 		log.info({}, `${release.ReleaseNumber} was created`);
		// 	} catch (e) {
		// 		log.error({ stack: e }, "Error creating new release");
		// 	}
		// 	// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
		// } catch (e: any) {
		// 	log.error(
		// 		{ stack: e?.response?.data },
		// 		`Error posting new release to opendock, ${release.ReleaseNumber}`,
		// 	);

		// 	return;
		// }

		log.info(
			{
				stack: {
					release: release.ReleaseNumber,
					releaseLoadtypeCheck,
					articleLoadType: opendDockArticleCheck?.loadType,
					defaultLoadType: defaultDock?.value,
					releaseOkToSchedule,
				},
			},
			`Skipping OpenDock post - release: ${release.ReleaseNumber} is not allowed to schedule`,
		);

		return;
	}

	await delay(750); // rate limit protection
};

export const monitorReleaseChanges = async () => {
	// TODO: validate if the setting for opendocks is active and start / stop the system based on this
	// if it changes we set to false and the next loop  will stop.

	const openDockMonitor = await db
		.select()
		.from(settings)
		.where(eq(settings.name, "opendock_sync"));
	// console.info("Starting release monitor", lastCheck);

	const sqlQuery = sqlQuerySelector(`releaseChecks`) as SqlQuery;

	if (!sqlQuery.success) {
		return returnFunc({
			success: false,
			level: "error",
			module: "datamart",
			subModule: "query",
			message: `Error getting releaseChecks info`,
			data: [sqlQuery.message],
			notify: false,
		});
	}

	if (openDockMonitor[0]?.active) {
		// const BUFFER_MS =
		// 	Math.floor(parseInt(openDockMonitor[0]?.value, 10) || 30) * 1.5 * 1000; // this should be >= to the interval we set in the cron TODO: should pull the buffer from the setting and give it an extra 10% then round to nearest int.

		createCronJob(
			"opendock_sync",
			`*/${parseInt(openDockMonitor[0]?.value, 10) || 30} * * * * *`,
			async () => {
				if (opendockSyncRunning) {
					log.warn(
						{},
						"Skipping opendock_sync because previous run is still active",
					);
					return;
				}

				opendockSyncRunning = true;
				try {
					// set this to the latest time.

					const result = await prodQuery(
						sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`),
						"Get release info",
					);

					log.debug(
						{ lastCheck },
						`${result.data.length} Changes to a release have been made`,
					);

					if (result.data.length) {
						for (const release of result.data) {
							await postRelease(release);

							// add a 2 seconds to account for a massive influx of orders and when we dont finish in 1 go it wont try to grab the same amount again
							const nDate = new Date(release.Upd_Date);
							nDate.setSeconds(nDate.getSeconds() + 2);

							lastCheck = formatInTimeZone(
								nDate.toISOString(),
								"UTC",
								"yyyy-MM-dd HH:mm:ss",
							);
							log.debug({ lastCheck }, "Changes to a release have been made");
							await delay(500);
						}
					}
				} catch (e) {
					console.error(
						{ error: e },
						"Error occurred while running the monitor job",
					);
					log.error(
						{ error: e },
						"Error occurred while running the monitor job",
					);
				} finally {
					opendockSyncRunning = false;
				}
			},
			"monitorReleaseChanges",
		);
	}
};
