/**
 * Simple Structured Logger
 */
import * as Sentry from '@sentry/cloudflare';
import type { LoggerConfig, ObjectContext, LogEntry, LogLevel } from './types';

export const DEFAULT_CONFIG: LoggerConfig = {
	level: 'info',
	prettyPrint: false,
};

const LOG_LEVELS: Record<LogLevel, number> = {
	debug: 0,
	info: 1,
	warn: 2,
	error: 3,
};

// Credential scrubbing patterns removed by request

/**
 * Scrubbing disabled: pass-through
 */
function scrubCredentials(data: unknown): unknown {
    return data;
}

export class StructuredLogger {
	private readonly component: string;
	private objectContext?: ObjectContext;
	private readonly config: LoggerConfig;
	private additionalFields: Record<string, unknown> = {};

	constructor(
		component: string,
		objectContext?: ObjectContext,
		config?: LoggerConfig,
	) {
		this.component = component;
		this.objectContext = objectContext ? { ...objectContext } : undefined;
		this.config = { ...DEFAULT_CONFIG, ...config };
	}

	/**
	 * Set the object ID dynamically
	 */
	setObjectId(id: string): void {
		if (!this.objectContext) {
			this.objectContext = { type: this.component, id };
		} else {
			this.objectContext.id = id;
		}
	}

	/**
	 * Set additional fields that will be included in all log entries
	 */
	setFields(fields: Record<string, unknown>): void {
		this.additionalFields = { ...this.additionalFields, ...fields };
	}

	/**
	 * Set a single field
	 */
	setField(key: string, value: unknown): void {
		this.additionalFields[key] = value;
	}

	/**
	 * Clear all additional fields
	 */
	clearFields(): void {
		this.additionalFields = {};
	}

	/**
	 * Create a child logger with extended context
	 */
	child(
		childContext: Partial<ObjectContext>,
		component?: string,
	): StructuredLogger {
		const newComponent = component || this.component;
		const mergedContext: ObjectContext = {
			type: childContext.type || 'ChildLogger',
			id: childContext.id || `child-${Date.now()}`,
			...childContext,
		};

		const childLogger = new StructuredLogger(
			newComponent,
			mergedContext,
			this.config,
		);
		childLogger.setFields(this.additionalFields);
		return childLogger;
	}

	/**
	 * Check if message should be logged based on level
	 */
	private shouldLog(level: LogLevel): boolean {
		const configLevel = LOG_LEVELS[this.config.level || 'info'];
		const messageLevel = LOG_LEVELS[level];
		return messageLevel >= configLevel;
	}

	/**
	 * Core logging method - builds structured JSON and outputs via console
	 */
	private log(
		level: LogLevel,
		message: string,
		data?: Record<string, unknown>,
		error?: Error,
	): void {
		if (!this.shouldLog(level)) return;

		const logEntry: LogEntry = {
			level,
			time: new Date().toISOString(),
			component: this.component,
			msg: scrubCredentials(message) as string,
		};

		// Add object context if available
		if (this.objectContext) {
			logEntry.object = { ...this.objectContext };
		}

		// Add additional fields with credential scrubbing
		if (Object.keys(this.additionalFields).length > 0) {
			const scrubbedAdditionalFields = scrubCredentials(this.additionalFields) as Record<string, unknown>;
			Object.assign(logEntry, scrubbedAdditionalFields);
		}

		// Add structured data with credential scrubbing
		if (data) {
			try {
				const scrubbedData = scrubCredentials(data);
				if (scrubbedData && typeof scrubbedData === 'object' && !Array.isArray(scrubbedData)) {
					Object.assign(logEntry, scrubbedData as Record<string, unknown>);
				}
			} catch {
				// If scrubbing fails, add a safe placeholder
				logEntry.data = '[DATA_SCRUB_FAILED]';
			}
		}

		// Add error details with credential scrubbing
		if (error instanceof Error) {
			logEntry.error = {
				name: error.name,
				message: scrubCredentials(error.message) as string,
				stack: scrubCredentials(error.stack) as string | undefined,
			};
		}

		// Output using appropriate method
		this.output(level, logEntry);
	}

	/**
	 * Safe JSON stringify that handles circular references
	 */
	private safeStringify(obj: unknown): string {
		const seen = new WeakSet();
		
		return JSON.stringify(obj, (_key, value) => {
			// Handle undefined, functions, symbols
			if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
				return undefined;
			}
			
			// Handle BigInt
			if (typeof value === 'bigint') {
				return value.toString();
			}
			
			// Handle circular references
			if (typeof value === 'object' && value !== null) {
				if (seen.has(value)) {
					return '[Circular]';
				}
				seen.add(value);
				
				// Handle Error objects specially to preserve stack traces
				if (value instanceof Error) {
					return {
						name: value.name,
						message: value.message,
						stack: value.stack,
						// Include any additional properties
						...Object.getOwnPropertyNames(value).reduce((acc, prop) => {
							if (!['name', 'message', 'stack'].includes(prop)) {
								const descriptor = Object.getOwnPropertyDescriptor(value, prop);
								if (descriptor && descriptor.enumerable) {
									acc[prop] = (value as any)[prop];
								}
							}
							return acc;
						}, {} as Record<string, unknown>)
					};
				}
			}
			
			return value;
		});
	}

	/**
	 * Output log entry using console methods
	 */
	private output(level: LogLevel, logEntry: LogEntry): void {
		const consoleMethod = this.getConsoleMethod(level);

		if (this.config.prettyPrint) {
			// Pretty formatted output for development
			const objectInfo = logEntry.object
				? `[${logEntry.object.type}${logEntry.object.id ? `#${logEntry.object.id}` : ''}]`
				: '';
			const prefix = `${logEntry.time} ${level.toUpperCase()} ${logEntry.component}${objectInfo}`;

			// Extract additional data excluding base fields
			const {
				level: _,
				time,
				component,
				msg,
				object,
				error,
				...additionalData
			} = logEntry;
			const hasAdditionalData = Object.keys(additionalData).length > 0;

			if (hasAdditionalData || error) {
				const extraInfo: Record<string, unknown> = {};
				if (hasAdditionalData) Object.assign(extraInfo, additionalData);
				if (error) extraInfo.error = error;
				console[consoleMethod](`${prefix}: ${msg}`, extraInfo);
			} else {
				console[consoleMethod](`${prefix}: ${msg}`);
			}
		} else {
			// Structured JSON output for production (optimal for Cloudflare Workers Logs)
			try {
				const jsonStr = this.safeStringify(logEntry);
				console[consoleMethod](jsonStr);
			} catch (e) {
				// Fallback if even safe stringify fails
				console[consoleMethod](
					JSON.stringify({
						level: logEntry.level,
						time: logEntry.time,
						component: logEntry.component,
						msg: '[LOG_STRINGIFY_FAILED]',
						stringifyError:
							e instanceof Error ? { name: e.name, message: e.message } : String(e),
					}),
				);
			}
		}
	}

	/**
	 * Get appropriate console method for log level
	 */
	private getConsoleMethod(
		level: LogLevel,
	): 'debug' | 'log' | 'warn' | 'error' {
		switch (level) {
			case 'debug':
				return 'debug';
			case 'info':
				return 'log';
			case 'warn':
				return 'warn';
			case 'error':
				return 'error';
			default:
				return 'log';
		}
	}

	/**
	 * Process variable arguments into structured data
	 */
	private processArgs(args: unknown[]): Record<string, unknown> {
		if (args.length === 0) return {};

		if (args.length === 1) {
			const arg = args[0];
			if (
				arg &&
				typeof arg === 'object' &&
				!Array.isArray(arg) &&
				!(arg instanceof Error)
			) {
				return arg as Record<string, unknown>;
			}
			return { data: arg };
		}

		// Multiple arguments
		const result: Record<string, unknown> = {};
		args.forEach((arg, index) => {
			if (
				arg &&
				typeof arg === 'object' &&
				!Array.isArray(arg) &&
				!(arg instanceof Error)
			) {
				Object.assign(result, arg as Record<string, unknown>);
			} else {
				result[`arg${index}`] = arg;
			}
		});
		return result;
	}

	/**
	 * Process arguments with error handling
	 */
	private processArgsWithError(args: unknown[]): {
		data: Record<string, unknown>;
		error?: Error;
	} {
		let error: Error | undefined;
		const otherArgs: unknown[] = [];

		const isErrorLike = (value: unknown): value is { name?: unknown; message?: unknown; stack?: unknown } => {
			return (
				value !== null &&
				typeof value === 'object' &&
				('message' in (value as Record<string, unknown>) || 'name' in (value as Record<string, unknown>))
			);
		};

		const toError = (value: { name?: unknown; message?: unknown; stack?: unknown }): Error => {
			const msg = typeof value.message === 'string' ? value.message : 'Unknown error';
			const err = new Error(msg);
			if (typeof value.name === 'string') err.name = value.name;
			if (typeof value.stack === 'string') (err as Error).stack = value.stack;
			return err;
		};

		args.forEach((arg) => {
			if (arg instanceof Error) {
				error = arg;
				return;
			}
			if (isErrorLike(arg)) {
				error = toError(arg);
				return;
			}
			otherArgs.push(arg);
		});

		return {
			data: this.processArgs(otherArgs),
			error,
		};
	}

	// Public logging methods
	debug(message: string, ...args: unknown[]): void {
		this.log('debug', message, this.processArgs(args));
	}

	info(message: string, ...args: unknown[]): void {
		this.log('info', message, this.processArgs(args));
	}

	warn(message: string, ...args: unknown[]): void {
		this.log('warn', message, this.processArgs(args));
	}

	error(message: string, ...args: unknown[]): void {
		const { data, error } = this.processArgsWithError(args);
        Sentry.captureException(error || new Error(message), { extra: data });
		this.log('error', message, data, error);
	}

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

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

/**
 * Create a basic structured logger
 */
export function createLogger(
	component: string,
	config?: LoggerConfig,
): StructuredLogger {
	return new StructuredLogger(component, undefined, config);
}

/**
 * Create logger with object context
 */
export function createObjectLogger(
	obj: unknown,
	component?: string,
	config?: LoggerConfig,
): StructuredLogger {
	const componentName = component || getObjectType(obj) || 'UnknownComponent';

	// Create basic object context without complex probing
	const objectContext: ObjectContext = {
		type: componentName,
	};

	// Try to get ID safely
	if (obj && typeof obj === 'object') {
		const objWithId = obj as Record<string, unknown>;
		if (
			objWithId.id &&
			(typeof objWithId.id === 'string' ||
				typeof objWithId.id === 'number')
		) {
			objectContext.id = String(objWithId.id);
		}
	}

	return new StructuredLogger(componentName, objectContext, config);
}

/**
 * Safely get object type
 */
function getObjectType(obj: unknown): string | undefined {
	try {
		if (obj && typeof obj === 'object') {
			return (obj as Record<string, unknown>).constructor?.name;
		}
		return typeof obj;
	} catch {
		return undefined;
	}
}


/**
 * Logger factory for global configuration
 */
export class LoggerFactory {
	private static globalConfig: LoggerConfig = DEFAULT_CONFIG;

	static configure(config: Partial<LoggerConfig>): void {
		this.globalConfig = { ...this.globalConfig, ...config };
	}

	static getConfig(): LoggerConfig {
		return { ...this.globalConfig };
	}

	static create(component: string): StructuredLogger {
		return new StructuredLogger(component, undefined, this.globalConfig);
	}

	static createForObject(obj: unknown, component?: string): StructuredLogger {
		return createObjectLogger(obj, component, this.globalConfig);
	}
}
