import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { PACKAGE_NAME } from "./constants.util.js";

/**
 * Format a timestamp for logging
 * @returns Formatted timestamp [HH:MM:SS]
 */
function getTimestamp(): string {
	const now = new Date();
	return `[${now.toISOString().split("T")[1].split(".")[0]}]`;
}

/**
 * Safely convert object to string with size limits
 * @param obj Object to stringify
 * @param maxLength Maximum length of the resulting string
 * @returns Safely stringified object
 */
function safeStringify(obj: unknown, maxLength = 1000): string {
	try {
		const str = JSON.stringify(obj);
		if (str.length <= maxLength) {
			return str;
		}
		return `${str.substring(0, maxLength)}... (truncated, ${str.length} chars total)`;
	} catch {
		return "[Object cannot be stringified]";
	}
}

/**
 * Extract essential values from larger objects for logging
 * @param obj The object to extract values from
 * @param keys Keys to extract (if available)
 * @returns Object containing only the specified keys
 */
function extractEssentialValues(
	obj: Record<string, unknown>,
	keys: string[],
): Record<string, unknown> {
	const result: Record<string, unknown> = {};
	keys.forEach((key) => {
		result[key] = obj[key];
	});
	return result;
}

/**
 * Format source path consistently using the standardized format:
 * [module/file.ts@function] or [module/file.ts]
 *
 * @param filePath File path (with or without src/ prefix)
 * @param functionName Optional function name
 * @returns Formatted source path according to standard pattern
 */
function formatSourcePath(filePath: string, functionName?: string): string {
	// Always strip 'src/' prefix for consistency
	const normalizedPath = filePath.replace(/^src\//, "");

	return functionName
		? `[${normalizedPath}@${functionName}]`
		: `[${normalizedPath}]`;
}

/**
 * Check if debug logging is enabled for a specific module
 *
 * This function parses the DEBUG environment variable to determine if a specific
 * module should have debug logging enabled. The DEBUG variable can be:
 * - 'true' or '1': Enable all debug logging
 * - Comma-separated list of modules: Enable debug only for those modules
 * - Module patterns with wildcards: e.g., 'controllers/*' enables all controllers
 *
 * Examples:
 * - DEBUG=true
 * - DEBUG=controllers/*,services/aws.sso.auth.service.ts
 * - DEBUG=transport,utils/formatter*
 *
 * @param modulePath The module path to check against DEBUG patterns
 * @returns true if debug is enabled for this module, false otherwise
 */
function isDebugEnabledForModule(modulePath: string): boolean {
	const debugEnv = process.env.DEBUG;

	if (!debugEnv) {
		return false;
	}

	// If debug is set to true or 1, enable all debug logging
	if (debugEnv === "true" || debugEnv === "1") {
		return true;
	}

	// Parse comma-separated debug patterns
	const debugPatterns = debugEnv.split(",").map((p) => p.trim());

	// Check if the module matches any pattern
	return debugPatterns.some((pattern) => {
		// Convert glob-like patterns to regex
		// * matches anything within a path segment
		// ** matches across path segments
		const regexPattern = pattern
			.replace(/\*/g, ".*") // Convert * to regex .*
			.replace(/\?/g, "."); // Convert ? to regex .

		const regex = new RegExp(`^${regexPattern}$`);
		return (
			regex.test(modulePath) ||
			// Check for pattern matches without the 'src/' prefix
			regex.test(modulePath.replace(/^src\//, ""))
		);
	});
}

// Generate a unique session ID for this process
const SESSION_ID = crypto.randomUUID();

// Get the package name from constants
const getPkgName = (): string => {
	return PACKAGE_NAME;
};

// MCP logs directory setup
const HOME_DIR = os.homedir();
const MCP_DATA_DIR = path.join(HOME_DIR, ".mcp", "data");
const CLI_NAME = getPkgName();

// Create the log file path with session ID
const LOG_FILENAME = `${CLI_NAME}.${SESSION_ID}.log`;
const LOG_FILEPATH = path.join(MCP_DATA_DIR, LOG_FILENAME);

// Flag to track if file logging is available
let FILE_LOGGING_AVAILABLE = false;

// Flag to track if we've attempted to initialize file logging
let FILE_LOGGING_INITIALIZED = false;

// Logger singleton to track initialization
let isLoggerInitialized = false;

// Lazy initialization of file logging
function initializeFileLogging(): void {
	if (FILE_LOGGING_INITIALIZED) {
		return;
	}

	FILE_LOGGING_INITIALIZED = true;

	try {
		// Ensure the MCP data directory exists
		if (!fs.existsSync(MCP_DATA_DIR)) {
			fs.mkdirSync(MCP_DATA_DIR, { recursive: true });
		}

		// Write initial log header
		fs.writeFileSync(
			LOG_FILEPATH,
			`# ${CLI_NAME} Log Session\n` +
				`Session ID: ${SESSION_ID}\n` +
				`Started: ${new Date().toISOString()}\n` +
				`Process ID: ${process.pid}\n` +
				`Working Directory: ${process.cwd()}\n` +
				`Command: ${process.argv.join(" ")}\n\n` +
				"## Log Entries\n\n",
			"utf8",
		);

		FILE_LOGGING_AVAILABLE = true;

		// Log initialization message only once
		if (!isLoggerInitialized) {
			console.info(
				`[${getTimestamp()}] [INFO] [domains/registry.ts] Logger initialized with session ID: ${SESSION_ID}`,
			);
			console.info(
				`[${getTimestamp()}] [INFO] [domains/registry.ts] Logs will be saved to: ${LOG_FILEPATH}`,
			);
			isLoggerInitialized = true;
		}
	} catch (error) {
		// File logging failed, but don't crash the process
		console.warn(
			`Warning: Could not initialize log file at ${LOG_FILEPATH}: ${error}. Logs will only appear in console.`,
		);
		FILE_LOGGING_AVAILABLE = false;
	}
}

/**
 * Logger class for consistent logging across the application.
 *
 * RECOMMENDED USAGE:
 *
 * 1. Create a file-level logger using the static forContext method:
 *    ```
 *    const logger = Logger.forContext('controllers/myController.ts');
 *    ```
 *
 * 2. For method-specific logging, create a method logger:
 *    ```
 *    const methodLogger = Logger.forContext('controllers/myController.ts', 'myMethod');
 *    ```
 *
 * 3. Avoid using raw string prefixes in log messages. Instead, use contextualized loggers.
 *
 * 4. For debugging objects, use the debugResponse method to log only essential properties.
 *
 * 5. Set DEBUG environment variable to control which modules show debug logs:
 *    - DEBUG=true (enable all debug logs)
 *    - DEBUG=controllers/*,services/* (enable for specific module groups)
 *    - DEBUG=transport,utils/formatter* (enable specific modules, supports wildcards)
 */
class Logger {
	private context?: string;
	private modulePath: string;
	private static sessionId = SESSION_ID;
	private static logFilePath = LOG_FILEPATH;

	constructor(context?: string, modulePath = "") {
		this.context = context;
		this.modulePath = modulePath;
	}

	/**
	 * Create a contextualized logger for a specific file or component.
	 * This is the preferred method for creating loggers.
	 *
	 * @param filePath The file path (e.g., 'controllers/aws.sso.auth.controller.ts')
	 * @param functionName Optional function name for more specific context
	 * @returns A new Logger instance with the specified context
	 *
	 * @example
	 * // File-level logger
	 * const logger = Logger.forContext('controllers/myController.ts');
	 *
	 * // Method-level logger
	 * const methodLogger = Logger.forContext('controllers/myController.ts', 'myMethod');
	 */
	static forContext(filePath: string, functionName?: string): Logger {
		return new Logger(formatSourcePath(filePath, functionName), filePath);
	}

	/**
	 * Create a method level logger from a context logger
	 * @param method Method name
	 * @returns A new logger with the method context
	 */
	forMethod(method: string): Logger {
		return Logger.forContext(this.modulePath, method);
	}

	private _formatMessage(message: string): string {
		return this.context ? `${this.context} ${message}` : message;
	}

	private _formatArgs(args: unknown[]): unknown[] {
		// If the first argument is an object and not an Error, safely stringify it
		if (
			args.length > 0 &&
			typeof args[0] === "object" &&
			args[0] !== null &&
			!(args[0] instanceof Error)
		) {
			args[0] = safeStringify(args[0]);
		}
		return args;
	}

	_log(
		level: "info" | "warn" | "error" | "debug",
		message: string,
		...args: unknown[]
	) {
		// Skip debug messages if not enabled for this module
		if (level === "debug" && !isDebugEnabledForModule(this.modulePath)) {
			return;
		}

		const timestamp = getTimestamp();
		const prefix = `${timestamp} [${level.toUpperCase()}]`;
		let logMessage = `${prefix} ${this._formatMessage(message)}`;

		const formattedArgs = this._formatArgs(args);
		if (formattedArgs.length > 0) {
			// Handle errors specifically
			if (formattedArgs[0] instanceof Error) {
				const error = formattedArgs[0] as Error;
				logMessage += ` Error: ${error.message}`;
				if (error.stack) {
					logMessage += `\n${error.stack}`;
				}
				// If there are more args, add them after the error
				if (formattedArgs.length > 1) {
					logMessage += ` ${formattedArgs
						.slice(1)
						.map((arg) => (typeof arg === "string" ? arg : safeStringify(arg)))
						.join(" ")}`;
				}
			} else {
				logMessage += ` ${formattedArgs.map((arg) => (typeof arg === "string" ? arg : safeStringify(arg))).join(" ")}`;
			}
		}

		// Initialize file logging on first use
		initializeFileLogging();

		// Write to log file if available
		if (FILE_LOGGING_AVAILABLE) {
			try {
				fs.appendFileSync(Logger.logFilePath, `${logMessage}\n`, "utf8");
			} catch (err) {
				// If we can't write to the log file, disable file logging and log the error to console
				FILE_LOGGING_AVAILABLE = false;
				console.error(
					`Failed to write to log file, disabling file logging: ${err}`,
				);
			}
		}

		if (process.env.NODE_ENV === "test") {
			console[level](logMessage);
		} else {
			console.error(logMessage);
		}
	}

	info(message: string, ...args: unknown[]) {
		this._log("info", message, ...args);
	}

	warn(message: string, ...args: unknown[]) {
		this._log("warn", message, ...args);
	}

	error(message: string, ...args: unknown[]) {
		this._log("error", message, ...args);
	}

	debug(message: string, ...args: unknown[]) {
		this._log("debug", message, ...args);
	}

	/**
	 * Log essential information about an API response
	 * @param message Log message
	 * @param response API response object
	 * @param essentialKeys Keys to extract from the response
	 */
	debugResponse(
		message: string,
		response: Record<string, unknown>,
		essentialKeys: string[],
	) {
		const essentialInfo = extractEssentialValues(response, essentialKeys);
		this.debug(message, essentialInfo);
	}

	/**
	 * Get the current session ID
	 * @returns The UUID for the current logging session
	 */
	static getSessionId(): string {
		return Logger.sessionId;
	}

	/**
	 * Get the current log file path
	 * @returns The path to the current log file
	 */
	static getLogFilePath(): string {
		return Logger.logFilePath;
	}
}

// Only export the Logger class to enforce contextual logging via Logger.forContext
export { Logger };
