import axios from "axios"
import { jwtDecode } from "jwt-decode"
import crypto from "crypto"
import retry from "async-retry"

import { t } from "../../../i18n"
import { ClineProvider } from "../../webview/ClineProvider"
import { TelemetryService } from "@roo-code/telemetry"
import { CostrictAuthConfig } from "../auth"
import { getClientId } from "../../../utils/getClientId"
import { ProviderSettings } from "@roo-code/types"

export interface IErrorMap {
	[code: string]: {
		message: string
		solution: string
	}
}

export class ErrorCodeManager {
	private static instance: ErrorCodeManager
	private errorMap: IErrorMap = {}
	private rawErrorMap: Map<string, any> = new Map()
	private unknownError = { message: t("apiErrors:status.unknown"), solution: t("apiErrors:solution.unknown") }
	private provider!: ClineProvider

	private constructor() {}

	public static getInstance(): ErrorCodeManager {
		if (!ErrorCodeManager.instance) {
			ErrorCodeManager.instance = new ErrorCodeManager()
		}
		return ErrorCodeManager.instance
	}

	/**
	 * Initialize error code manager
	 * @param provider ClineProvider instance
	 */
	public async initialize(provider: ClineProvider): Promise<void> {
		try {
			this.provider = provider
			await this.refreshErrorCodes()
		} catch (error) {
			console.error("Failed to initialize ErrorCodeManager:", error)
		}
	}

	/**
	 * Refresh error code mapping
	 */
	public async refreshErrorCodes(): Promise<void> {
		try {
			await retry(
				async () => {
					// Clear existing error code mapping
					this.errorMap = {}
					// Get remote error codes
					const remoteErrorMap = await this.fetchRemoteCodes()
					if (Object.keys(remoteErrorMap).length) {
						this.errorMap = remoteErrorMap
						this.provider.setValue("errorCode", remoteErrorMap)
					}
				},
				{
					retries: 2,
				},
			)
		} catch (error) {
			const { errorCode } = await this.provider.getState()
			this.errorMap = (errorCode ?? {}) as IErrorMap
			console.error("Failed to refresh error codes:", error)
		}
	}

	/**
	 * Fetch error codes from remote
	 * @returns Promise<IErrorMap> Remote error code mapping
	 */
	private async fetchRemoteCodes(): Promise<IErrorMap> {
		try {
			const { language, apiConfiguration } = await this.provider.getState()
			const baseUrl = apiConfiguration.costrictBaseUrl || CostrictAuthConfig.getInstance().getDefaultApiBaseUrl()
			const response = await axios.get(`${baseUrl}/costrict-static/error-code/error_codes_${language}.json`, {
				headers: {
					"zgsm-request-id": getClientId(),
				},
			})
			return response.data
		} catch (error) {
			console.error("Failed to fetch remote error codes:", error)
			throw error
			// Return empty object as fallback for any exception
			// return {}
		}
	}
	/**
	 * Parse ZGSM JWT token information
	 * @param costrictAccessToken JWT token
	 * @returns Object containing expiration time, update time, and old mode login status
	 */
	private parseCostrictTokenInfo(costrictAccessToken?: string): {
		costrictApiKeyExpiredAt: string
		costrictApiKeyUpdatedAt: string
		isOldModeLoginState: boolean
		expTimestamp: number
		iatTimestamp: number
	} {
		if (!costrictAccessToken) {
			return {
				costrictApiKeyExpiredAt: "",
				costrictApiKeyUpdatedAt: "",
				isOldModeLoginState: false,
				expTimestamp: 0,
				iatTimestamp: 0,
			}
		}

		try {
			const { exp, iat, universal_id } = jwtDecode(costrictAccessToken) as any
			return {
				costrictApiKeyExpiredAt: new Date(exp * 1000).toLocaleString(),
				costrictApiKeyUpdatedAt: new Date(iat * 1000).toLocaleString(),
				isOldModeLoginState: !universal_id,
				expTimestamp: exp * 1000,
				iatTimestamp: iat * 1000,
			}
		} catch (error) {
			console.error("Failed to decode ZGSM access token:", error)
			return {
				costrictApiKeyExpiredAt: "",
				costrictApiKeyUpdatedAt: "",
				isOldModeLoginState: false,
				expTimestamp: 0,
				iatTimestamp: 0,
			}
		}
	}

	/**
	 * Parse error response
	 * @param errorCode Error code
	 * @returns Formatted error information object
	 */
	public async parseResponse(error: any, isCostrict = false, taskId: string, instanceId: string): Promise<string> {
		const isHtml = error?.headers && error.headers["content-type"] && error.headers["content-type"] === "text/html"
		let rawError =
			(error.error?.metadata?.raw ? JSON.stringify(error.error.metadata.raw, null, 2) : error.message) ||
			"Unknown error"

		if (!isCostrict) return rawError
		this.unknownError.message = rawError
		let status = error.status as number
		const { code, headers } = error
		const requestId = (headers && headers?.get?.("x-request-id")) ?? null
		const { apiConfiguration, errorCode } = await this.provider.getState()
		const { costrictApiKeyExpiredAt, costrictApiKeyUpdatedAt, isOldModeLoginState, expTimestamp } =
			this.parseCostrictTokenInfo(apiConfiguration.costrictAccessToken)
		const now = Date.now()
		const isAuthServiceError = expTimestamp > 0 && expTimestamp > now
		const defaultError = {
			401: {
				message: isOldModeLoginState
					? t("apiErrors:status.old_mode_token")
					: isAuthServiceError
						? t("apiErrors:status.401_auth_service_error")
						: t("apiErrors:status.401", {
								exp: costrictApiKeyExpiredAt || "-",
								iat: costrictApiKeyUpdatedAt || "-",
							}),
				solution: isAuthServiceError
					? t("apiErrors:solution.401_auth_service_error")
					: t("apiErrors:solution.401"),
			},
			400: { message: rawError || t("apiErrors:status.400"), solution: t("apiErrors:solution.400") },
			403: { message: rawError || t("apiErrors:status.403"), solution: t("apiErrors:solution.403") },
			404: { message: isHtml ? t("apiErrors:status.404") : rawError, solution: t("apiErrors:solution.404") },
			429: { message: rawError || t("apiErrors:status.429"), solution: t("apiErrors:solution.429") },
			413: { message: rawError || t("apiErrors:status.413"), solution: t("apiErrors:solution.413") },
			500: { message: isHtml ? t("apiErrors:status.500") : rawError, solution: t("apiErrors:solution.500") },
			502: { message: isHtml ? t("apiErrors:status.502") : rawError, solution: t("apiErrors:solution.502") },
			503: { message: isHtml ? t("apiErrors:status.503") : rawError, solution: t("apiErrors:solution.503") },
			504: { message: isHtml ? t("apiErrors:status.504") : rawError, solution: t("apiErrors:solution.504") },
			undefined: {
				message: rawError || t("apiErrors:status.undefined"),
				solution: t("apiErrors:solution.undefined"),
			},
		} as Record<string | number, { message: string; solution: string }>
		// List of error codes that require authentication
		const authRequiredCodes = [
			"ai-gateway.unauthorized",
			"quota-manager.unauthorized",
			"quota-manager.token_invalid",
			"quota-manager.voucher_expired",
		]
		if (code) {
			const errorMap = Object.keys(this.errorMap).length > 0 ? this.errorMap : (errorCode ?? {}) // Use fetched error codes or fallback to provider state
			let { message, solution } = errorMap[code] || this.unknownError
			if (authRequiredCodes.includes(code)) {
				rawError = message
				message = defaultError["401"].message
				solution = defaultError["401"].solution
				error.status = status = 401
			} else if (code === "ai-gateway.insufficient_quota") {
				solution = await this.handleInsufficientQuotaError(apiConfiguration)
			} else if (code === "ai-gateway.star_required") {
				solution = await this.handleStarRequiredError(apiConfiguration)
			}
			TelemetryService.instance.captureError(`ApiError_${code}`)
			this.provider.log(`[CoStrict#apiErrors] task ${taskId}.${instanceId} Raw Error: ${rawError}`)
			return `${t("apiErrors:request.error_details")}\n\n${message}\n\n${requestId ? `RequestID: ${requestId}\n\n` : ""}${t("apiErrors:request.solution")}\n${solution}`
		}
		const { message, solution } = defaultError[status] || this.unknownError
		if (defaultError[status]) {
			TelemetryService.instance.captureError(
				status === undefined ? `ApiError_unknown` : `ApiError_status_${status}`,
			)
		} else {
			TelemetryService.instance.captureError(`ApiError_unknown`)
		}
		this.provider.log(`[CoStrict#apiErrors] task ${taskId}.${instanceId} Raw Error: ${rawError}`)
		return `${t("apiErrors:request.error_details")}\n\n${message}\n\n${requestId ? `RequestID: ${requestId}\n\n` : ""}${t("apiErrors:request.solution")}\n${solution}`
	}

	/**
	 * Handling insufficient quota errors
	 * @param apiConfiguration api config
	 * @returns string
	 */
	private async handleInsufficientQuotaError(apiConfiguration: ProviderSettings): Promise<string> {
		const hash = await this.hashToken(apiConfiguration.costrictAccessToken || "")
		const baseurl = apiConfiguration.costrictBaseUrl || CostrictAuthConfig.getInstance().getDefaultLoginBaseUrl()

		const solution1 = t("apiErrors:solution.ai-gateway.insufficientCredits")
		const solution2 = t("apiErrors:solution.ai-gateway.quotaAcquisition")

		const activitiesStr = `${t("apiErrors:solution.activitiesStr.participate")}<a href='${baseurl}/credit/manager/?state=${hash}&tab=activity' style="font-size: 12px;color:#0078d4;text-decoration: none;">${t("apiErrors:solution.activitiesStr.operationalActivities")}</a> ${t("apiErrors:solution.activitiesStr.or")} <a href='${baseurl}/credit/manager/?state=${hash}&tab=subscription' style="font-size: 12px;color:#0078d4;text-decoration: none;">${t("apiErrors:solution.activitiesStr.purchaseQuota")}</a> ${t("apiErrors:solution.activitiesStr.getCreditLimit")}`

		return `
<span style="color:#E64545;font-size: 12px;">${solution1}</span> <a href='${baseurl}/credit/manager/md-preview' style="font-size: 12px;color:#0078d4;text-decoration: none;">${solution2}</a>
\n${activitiesStr}
`
	}

	/**
	 * Handling errors that require starring
	 * @param apiConfiguration api config
	 * @returns string
	 */
	private async handleStarRequiredError(apiConfiguration: any): Promise<string> {
		const hash = await this.hashToken(apiConfiguration.costrictAccessToken || "")
		const baseurl = apiConfiguration.costrictBaseUrl || CostrictAuthConfig.getInstance().getDefaultLoginBaseUrl()

		const solution1 = t("apiErrors:solution.ai-gateway.pleaseStarProject")
		const solution2 = t("apiErrors:solution.ai-gateway.howToStar")

		const checkRemainingQuotaStr = `${t("apiErrors:solution.quota-check.checkRemainingQuota")} " <a href='${baseurl}/credit/manager/?state=${hash}&tab=usage' style="font-size: 12px;color:#0078d4;text-decoration: none;">${t("apiErrors:solution.quota-check.creditUsageStats")}</a> " ${t("apiErrors:solution.quota-check.viewDetails")}`

		return `
<span style="color:#E64545;font-size: 12px;">${solution1}</span> <a href='${baseurl}/credit/manager/md-preview' style="font-size: 12px;color:#0078d4;text-decoration: none;">${solution2}</a>
\n${checkRemainingQuotaStr}
`
	}

	private async hashToken(token: string) {
		const encoder = new TextEncoder()
		const data = encoder.encode(token)
		const hashBuffer = await crypto.subtle.digest("SHA-256", data)
		return Array.from(new Uint8Array(hashBuffer))
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("")
	}

	getErrorMessageByCode(code: string) {
		return this.errorMap[code] || this.unknownError
	}

	/**
	 * Set raw error by request ID
	 * @param requestId Request ID
	 * @param error Raw error object
	 */
	public setRawError(requestId: string, error: any): void {
		this.rawErrorMap.set(requestId, error)
	}

	/**
	 * Get raw error by request ID
	 * @param requestId Request ID
	 * @returns Raw error object or undefined
	 */
	public getRawError(requestId: string): any {
		return this.rawErrorMap.get(requestId)
	}

	/**
	 * Delete raw error by request ID
	 * @param requestId Request ID
	 * @returns True if error was deleted, false if not found
	 */
	public deleteRawError(requestId: string): boolean {
		return this.rawErrorMap.delete(requestId)
	}
}
