import { sign, verify, TokenExpiredError } from 'jsonwebtoken';
import { createHash, randomUUID } from 'crypto';
import { GenericError } from '#interfaces/error/generic';
import Config from '#config';

export interface JWTPayload {
	sub: string; // Subject (user ID)
	id: string; // Alias for sub (backward compatibility)
	sid?: string | undefined; // Session ID (for session tracking and revocation)
	jti?: string | undefined; // JWT ID (for token-level revocation)
	aud: string; // Audience
	iss: string; // Issuer
	iat: number; // Issued at timestamp
	exp: number; // Expiration timestamp
	email: string;
	roles: string[];
	resident_id?: string | null;
	permissions?: string[];
	roleIds?: string[];
	username?: string;
	tenantId?: string | null;
	type?: 'access' | 'refresh';
}

export interface TokenUserInput {
	id: string;
	email: string;
	roles?: string[];
	username?: string;
	tenantId?: string | null;
	sessionId?: string; // Session ID for sid claim
	residentId?: string | null;
}

export interface CookieOptions {
	httpOnly: boolean;
	secure: boolean;
	sameSite: 'lax' | 'strict' | 'none';
	maxAge: number;
	path: string;
	domain?: string;
}

export class TokenService {
	private static getJwtSecret(): string {
		const secret = Config.auth.jwtSecret;
		if (!secret) {
			throw new Error('JWT_SECRET environment variable is required');
		}
		return secret;
	}
	private static getJwtRefreshSecret(): string {
		const secret = Config.auth.jwtRefreshSecret;
		if (!secret) {
			throw new Error('JWT_REFRESH_SECRET environment variable is required');
		}
		return secret;
	}
	private static getJwtAccessExpiresIn(): string {
		return Config.auth.jwtExpiresIn || '15m';
	}
	private static getJwtRefreshExpiresIn(): string {
		return Config.auth.jwtRefreshExpiresIn || '7d';
	}

	// Validate required secrets — can be called at startup for fail-fast behaviour
	static validateSecrets() {
		if (!this.getJwtSecret()) {
			throw new Error('JWT_SECRET environment variable is required');
		}
		if (!this.getJwtRefreshSecret()) {
			throw new Error('JWT_REFRESH_SECRET environment variable is required');
		}
	}

	/**
	 * Get secure cookie options for access tokens.
	 * When COOKIE_CROSS_SITE=true (frontend on a different domain/origin),
	 * SameSite is set to 'none' and Secure is forced to true.
	 */
	static getAccessTokenCookieOptions(): CookieOptions {
		// eslint-disable-next-line no-restricted-syntax
		const isProduction = process.env.NODE_ENV === 'production'; // NODE_ENV is acceptable for environment detection
		// eslint-disable-next-line no-restricted-syntax
		const isStaging = process.env.NODE_ENV === 'staging';
		const isCrossSite = Config.cookie.crossSite;
		const forceSecure = Config.cookie.forceSecure;
		const devSameSiteLax = Config.cookie.devSameSiteLax;
		const basePath = Config.cookie.basePath || '';
		const maxAge = this.parseTimeToSeconds(this.getJwtAccessExpiresIn()) * 1000;
		const path = `${basePath}/api`;
		console.log(`[Cookie] Access token path: ${path}, basePath: ${basePath}`);
		return {
			httpOnly: true,
			// SameSite=None requires Secure=true (browser requirement)
			secure: forceSecure || isCrossSite || isProduction || isStaging,
			sameSite: devSameSiteLax ? 'lax' : isCrossSite ? 'none' : 'lax',
			maxAge,
			path,
			...((isProduction || isStaging) && Config.cookie.domain ? { domain: Config.cookie.domain } : {}),
		};
	}

	/**
	 * Get secure cookie options for refresh tokens.
	 * When COOKIE_CROSS_SITE=true, SameSite is 'none' and Secure is forced.
	 */
	static getRefreshTokenCookieOptions(): CookieOptions {
		// eslint-disable-next-line no-restricted-syntax
		const isProduction = process.env.NODE_ENV === 'production';
		// eslint-disable-next-line no-restricted-syntax
		const isStaging = process.env.NODE_ENV === 'staging';
		const isCrossSite = Config.cookie.crossSite;
		const forceSecure = Config.cookie.forceSecure;
		const devSameSiteLax = Config.cookie.devSameSiteLax;
		const basePath = Config.cookie.basePath || '';
		const maxAge = this.parseTimeToSeconds(this.getJwtRefreshExpiresIn()) * 1000;
		return {
			httpOnly: true,
			secure: forceSecure || isCrossSite || isProduction || isStaging,
			sameSite: devSameSiteLax ? 'lax' : isCrossSite ? 'none' : 'lax',
			maxAge,
			path: `${basePath}/api/v1/auth/refresh`,
			...((isProduction || isStaging) && Config.cookie.domain ? { domain: Config.cookie.domain } : {}),
		};
	}

	/**
	 * Generate JWT access token with standard claims
	 */
	static generateAccessToken(user: TokenUserInput): string {
		this.validateSecrets();
		const now = Math.floor(Date.now() / 1000);
		const payload = {
			sub: user.id,
			id: user.id, // Alias for backward compatibility
			sid: user.sessionId,
			jti: randomUUID(), // Unique token ID for revocation
			aud: 'api-users',
			iss: 'backend-template',
			iat: now,
			email: user.email,
			roles: user.roles || [],
		};

		if (user.username) {
			(payload as any).username = user.username;
		}

		if (user.residentId) {
			(payload as any).resident_id = user.residentId;
		}

		return sign(payload, this.getJwtSecret() as any, {
			expiresIn: this.getJwtAccessExpiresIn() as any,
		});
	}

	/**
	 * Generate JWT refresh token with standard claims
	 */
	static generateRefreshToken(user: TokenUserInput): string {
		this.validateSecrets();
		const now = Math.floor(Date.now() / 1000);
		const payload = {
			sub: user.id,
			id: user.id, // Alias for backward compatibility
			sid: user.sessionId,
			jti: randomUUID(), // Unique token ID for revocation
			aud: 'api-users',
			iss: 'backend-template',
			iat: now,
			email: user.email,
			roles: user.roles || [],
			type: 'refresh',
		};

		if (user.username) {
			(payload as any).username = user.username;
		}

		if (user.residentId) {
			(payload as any).resident_id = user.residentId;
		}

		return sign(payload, this.getJwtRefreshSecret() as any, {
			expiresIn: this.getJwtRefreshExpiresIn() as any,
		});
	}

	/**
	 * Verify JWT access token
	 */
	static verifyAccessToken(token: string): JWTPayload {
		this.validateSecrets();
		try {
			const decoded = verify(token, this.getJwtSecret()) as JWTPayload;
			return decoded;
		} catch (error) {
			if (error instanceof TokenExpiredError) {
				throw new GenericError('TOKEN_EXPIRED');
			}
			throw new GenericError('UNAUTHORIZED');
		}
	}

	/**
	 * Verify JWT refresh token
	 */
	static verifyRefreshToken(token: string): JWTPayload {
		this.validateSecrets();
		try {
			const decoded = verify(token, this.getJwtRefreshSecret()) as JWTPayload;
			return decoded;
		} catch (error) {
			if (error instanceof TokenExpiredError) {
				throw new GenericError('TOKEN_EXPIRED');
			}
			throw new GenericError('UNAUTHORIZED');
		}
	}

	/**
	 * Parse time string to seconds (e.g., '15m' -> 900)
	 */
	static parseTimeToSeconds(timeStr: string | undefined): number {
		if (!timeStr) return 900; // 15 minutes default
		const regex = /^(\d+)([smhd])$/;
		const match = timeStr.match(regex);
		if (!match) return 900;

		const value = parseInt(match[1]!);
		const unit = match[2]!;

		switch (unit) {
			case 's':
				return value;
			case 'm':
				return value * 60;
			case 'h':
				return value * 3600;
			case 'd':
				return value * 86400;
			default:
				return 900;
		}
	}

	/**
	 * Hash token for audit logging (SHA-256)
	 */
	static hashToken(token: string): string {
		return createHash('sha256').update(token).digest('hex');
	}
}
