import * as Sentry from "@sentry/node";
import {
	CommandClass,
	isCommandClassContainer,
	isEncapsulatingCommandClass,
} from "@zwave-js/cc";
import {
	DataDirection,
	getDirectionPrefix,
	LogContext,
	MessagePriority,
	messageRecordToLines,
	tagify,
	ZWaveLogContainer,
	ZWaveLoggerBase,
} from "@zwave-js/core";
import type { Message, ResponseRole } from "@zwave-js/serial";
import { FunctionType, MessageType } from "@zwave-js/serial";
import { getEnumMemberName } from "@zwave-js/shared";
import type { SortedList } from "alcalzone-shared/sorted-list";
import type { Driver } from "../driver/Driver";
import type { Transaction } from "../driver/Transaction";
import { NodeStatus } from "../node/_Types";

export const DRIVER_LABEL = "DRIVER";
const DRIVER_LOGLEVEL = "verbose";
const SENDQUEUE_LOGLEVEL = "debug";

export interface DriverLogContext extends LogContext<"driver"> {
	direction?: DataDirection;
}

export class DriverLogger extends ZWaveLoggerBase<DriverLogContext> {
	constructor(private readonly driver: Driver, loggers: ZWaveLogContainer) {
		super(loggers, DRIVER_LABEL);
	}

	private isDriverLogVisible(): boolean {
		return this.container.isLoglevelVisible(DRIVER_LOGLEVEL);
	}

	private isSendQueueLogVisible(): boolean {
		return this.container.isLoglevelVisible(SENDQUEUE_LOGLEVEL);
	}

	/**
	 * Logs a message
	 * @param msg The message to output
	 */
	public print(
		message: string,
		level?: "debug" | "verbose" | "warn" | "error" | "info",
	): void {
		const actualLevel = level || DRIVER_LOGLEVEL;
		if (!this.container.isLoglevelVisible(actualLevel)) return;

		this.logger.log({
			level: actualLevel,
			message,
			direction: getDirectionPrefix("none"),
			context: { source: "driver", direction: "none" },
		});
	}

	/**
	 * Serializes a message that starts a transaction, i.e. a message that is sent and may expect a response
	 */
	public transaction(transaction: Transaction): void {
		if (!this.isDriverLogVisible()) return;

		const { message } = transaction;
		// On the first attempt, we print the basic information about the transaction
		const secondaryTags: string[] = [];
		// TODO: restore logging
		// if (transaction.sendAttempts === 1) {
		secondaryTags.push(`P: ${MessagePriority[transaction.priority]}`);
		// } else {
		// 	// On later attempts, we print the send attempts
		// 	secondaryTags.push(
		// 		`attempt ${transaction.sendAttempts}/${transaction.maxSendAttempts}`,
		// 	);
		// }

		this.logMessage(message, {
			secondaryTags,
			// Since we are programming a controller, the first message of a transaction is always outbound
			// (not to confuse with the message type, which may be Request or Response)
			direction: "outbound",
		});
	}

	/** Logs information about a message that is received as a response to a transaction */
	public transactionResponse(
		message: Message,
		originalTransaction: Transaction | undefined,
		role: ResponseRole,
	): void {
		if (!this.isDriverLogVisible()) return;
		this.logMessage(message, {
			nodeId: originalTransaction?.message?.getNodeId(),
			secondaryTags: [role],
			direction: "inbound",
		});
	}

	public logMessage(
		message: Message,
		{
			// Used to relate this log message to a node
			nodeId,
			secondaryTags,
			direction = "none",
		}: {
			nodeId?: number;
			secondaryTags?: string[];
			direction?: DataDirection;
		} = {},
	): void {
		if (!this.isDriverLogVisible()) return;
		if (nodeId == undefined) nodeId = message.getNodeId();
		if (nodeId != undefined && !this.container.shouldLogNode(nodeId)) {
			return;
		}

		const isCCContainer = isCommandClassContainer(message);
		const logEntry = message.toLogEntry();

		let msg: string[] = [tagify(logEntry.tags)];
		if (logEntry.message) {
			msg.push(
				...messageRecordToLines(logEntry.message).map(
					(line) => (isCCContainer ? "│ " : "  ") + line,
				),
			);
		}

		try {
			// If possible, include information about the CCs
			if (isCommandClassContainer(message)) {
				// Remove the default payload message and draw a bracket
				msg = msg.filter((line) => !line.startsWith("│ payload:"));

				let indent = 0;
				let cc: CommandClass = message.command;
				while (true) {
					const isEncapCC = isEncapsulatingCommandClass(cc);
					const loggedCC = cc.toLogEntry(this.driver);
					msg.push(
						" ".repeat(indent * 2) + "└─" + tagify(loggedCC.tags),
					);

					indent++;
					if (loggedCC.message) {
						msg.push(
							...messageRecordToLines(loggedCC.message).map(
								(line) =>
									`${" ".repeat(indent * 2)}${
										isEncapCC ? "│ " : "  "
									}${line}`,
							),
						);
					}
					// If this is an encap CC, continue
					if (isEncapsulatingCommandClass(cc)) {
						cc = cc.encapsulated;
					} else {
						break;
					}
				}
			}

			this.logger.log({
				level: DRIVER_LOGLEVEL,
				secondaryTags:
					secondaryTags && secondaryTags.length > 0
						? tagify(secondaryTags)
						: undefined,
				message: msg,
				// Since we are programming a controller, responses are always inbound
				// (not to confuse with the message type, which may be Request or Response)
				direction: getDirectionPrefix(direction),
				context: { source: "driver", direction },
			});
		} catch (e) {
			// When logging fails, send the message to Sentry
			try {
				Sentry.captureException(e);
			} catch {}
		}
	}

	/** Logs what's currently in the driver's send queue */
	public sendQueue(queue: SortedList<Transaction>): void {
		if (!this.isSendQueueLogVisible()) return;

		let message = "Send queue:";
		if (queue.length > 0) {
			for (const trns of queue) {
				// TODO: This formatting should be shared with the other logging methods
				const node = trns.message.getNodeUnsafe(this.driver);
				const prefix =
					trns.message.type === MessageType.Request
						? "[REQ]"
						: "[RES]";
				const postfix =
					node != undefined
						? ` [Node ${node.id}, ${getEnumMemberName(
								NodeStatus,
								node.status,
						  )}]`
						: "";
				const command = isCommandClassContainer(trns.message)
					? ` (${trns.message.command.constructor.name})`
					: "";
				message += `\n· ${prefix} ${
					FunctionType[trns.message.functionType]
				}${command}${postfix}`;
			}
		} else {
			message += " (empty)";
		}
		this.logger.log({
			level: SENDQUEUE_LOGLEVEL,
			message,
			secondaryTags: `(${queue.length} message${
				queue.length === 1 ? "" : "s"
			})`,
			direction: getDirectionPrefix("none"),
			context: { source: "driver", direction: "none" },
		});
	}
}
