/* eslint-disable no-restricted-syntax */
import { IpcChannel } from '@shared/IpcChannel'
import type { LogContextData, LogLevel, LogSourceWithContext } from '@shared/types/logger'
import { LEVEL, LEVEL_MAP } from '@shared/types/logger'

// check if the current process is a worker
const IS_WORKER = typeof window === 'undefined'
// check if we are in the dev env
// DO NOT use `constants.ts` here, because the files contains other dependencies that will fail in worker process
const IS_DEV = IS_WORKER ? false : window.electron?.process?.env?.NODE_ENV === 'development'
// CS_DIAGNOSTICS makes a packaged build behave like dev for logging (verbose level,
// console output, and the CSLOGGER_RENDERER_* overrides all turn on). Idempotent in dev.
// The flag reaches the renderer via the preload-exposed process.env, same path as in main.
const DIAGNOSTICS_ENABLED = IS_WORKER ? false : !!window.electron?.process?.env?.CS_DIAGNOSTICS
const DEV_LOGGING = IS_DEV || DIAGNOSTICS_ENABLED

const DEFAULT_LEVEL = DEV_LOGGING ? LEVEL.SILLY : LEVEL.INFO
const MAIN_LOG_LEVEL = LEVEL.WARN

// Each renderer window declares its logger source in its `index.html` via
// `<meta name="logger-window-source" content="...">`. Keep this attribute name
// in sync with those HTML files (see src/renderer/windows/README.md).
const WINDOW_SOURCE_META_NAME = 'logger-window-source'

/**
 * Read the window source declared by the `<meta name="logger-window-source">` tag.
 *
 * The meta tag is parsed before any module script runs, so the source is available
 * the moment LoggerService is constructed — no ordering guarantees needed at call
 * sites. Returns '' when there is no document (worker) or no meta tag, letting an
 * explicit {@link LoggerService.initWindowSource} take over.
 */
export function resolveWindowSourceFromMeta(doc: Document | undefined): string {
  const meta = doc?.querySelector(`meta[name="${WINDOW_SOURCE_META_NAME}"]`)
  return meta?.getAttribute('content')?.trim() ?? ''
}

/**
 * IMPORTANT: How to use LoggerService
 * please refer to
 *   English: `docs/technical/how-to-use-logger-en.md`
 *   Chinese: `docs/technical/how-to-use-logger-zh.md`
 */
export class LoggerService {
  // env variables, only used in dev / diagnostics (CS_DIAGNOSTICS) mode
  // only affect console output, not affect logToMain
  private envLevel: LogLevel = LEVEL.NONE
  private envShowModules: string[] = []

  private level: LogLevel = DEFAULT_LEVEL
  private logToMainLevel: LogLevel = MAIN_LOG_LEVEL

  // Explicit source set via initWindowSource(); takes precedence over derivedWindow.
  private window: string = ''
  // Source derived from the window's <meta name="logger-window-source"> at construction.
  private derivedWindow: string = ''
  private module: string = ''
  private context: Record<string, any> = {}

  constructor() {
    this.derivedWindow = resolveWindowSourceFromMeta(typeof document === 'undefined' ? undefined : document)

    if (DEV_LOGGING) {
      if (
        window.electron?.process?.env?.CSLOGGER_RENDERER_LEVEL &&
        Object.values(LEVEL).includes(window.electron?.process?.env?.CSLOGGER_RENDERER_LEVEL as LogLevel)
      ) {
        this.envLevel = window.electron?.process?.env?.CSLOGGER_RENDERER_LEVEL as LogLevel

        console.log(
          `%c[LoggerService] env CSLOGGER_RENDERER_LEVEL loaded: ${this.envLevel}`,
          'color: blue; font-weight: bold'
        )
      }

      if (window.electron?.process?.env?.CSLOGGER_RENDERER_SHOW_MODULES) {
        const showModules = window.electron?.process?.env?.CSLOGGER_RENDERER_SHOW_MODULES.split(',')
          .map((module) => module.trim())
          .filter((module) => module !== '')
        if (showModules.length > 0) {
          this.envShowModules = showModules

          console.log(
            `%c[LoggerService] env CSLOGGER_RENDERER_SHOW_MODULES loaded: ${this.envShowModules.join(' ')}`,
            'color: blue; font-weight: bold'
          )
        }
      }
    }
  }

  /**
   * Explicitly set the window source, overriding the `<meta>`-derived default.
   *
   * Windows normally declare their source via `<meta name="logger-window-source">`
   * and never call this. Use it where no meta exists — e.g. workers
   * (`initWindowSource('Worker')`) or tests. Can only be set once; later calls warn.
   * @param window - The window identifier
   * @returns The logger service instance
   */
  public initWindowSource(window: string): LoggerService {
    if (this.window) {
      console.warn(
        '[LoggerService] window source already initialized, current: %s, want to set: %s',
        this.window,
        window
      )
      return this
    }
    this.window = window
    return this
  }

  /**
   * Create a new logger with module name and additional context
   * @param module - The module name for logging
   * @param context - Additional context data
   * @returns A new logger instance with the specified context
   */
  public withContext(module: string, context?: Record<string, any>): LoggerService {
    const newLogger = Object.create(this)

    // Copy all properties from the base logger
    newLogger.module = module
    newLogger.context = { ...this.context, ...context }

    return newLogger
  }

  /**
   * Process and output log messages based on level and configuration
   * @param level - The log level
   * @param message - The log message
   * @param data - Additional data to log
   */
  private processLog(level: LogLevel, message: string, data: any[]): void {
    // Precedence: explicit initWindowSource() > <meta>-derived source > UNKNOWN fallback.
    let windowSource = this.window || this.derivedWindow
    if (!windowSource) {
      console.error('[LoggerService] window source not initialized, please initialize window source first')
      windowSource = 'UNKNOWN'
    }

    const currentLevel = LEVEL_MAP[level]

    // if in dev / diagnostics mode, check if the env variables are set and use the env level and show modules to skip logs
    if (DEV_LOGGING) {
      if (this.envLevel !== LEVEL.NONE && currentLevel < LEVEL_MAP[this.envLevel]) {
        return
      }
      if (this.module && this.envShowModules.length > 0 && !this.envShowModules.includes(this.module)) {
        return
      }
    }

    // skip log if level is lower than default level
    if (currentLevel < LEVEL_MAP[this.level]) {
      return
    }

    const logMessage = this.module ? `[${this.module}] ${message}` : message

    switch (level) {
      case LEVEL.ERROR:
        console.error('%c<error>', 'color: red; font-weight: bold', logMessage, ...data)
        break
      case LEVEL.WARN:
        console.warn('%c<warn>', 'color: #FFA500; font-weight: bold', logMessage, ...data)
        break
      case LEVEL.INFO:
        console.info('%c<info>', 'color: #32CD32; font-weight: bold', logMessage, ...data)
        break
      case LEVEL.VERBOSE:
        console.debug('%c<verbose>', 'color: #808080', logMessage, ...data)
        break
      case LEVEL.DEBUG:
        console.debug('%c<debug>', 'color: #7B68EE', logMessage, ...data)
        break
      case LEVEL.SILLY:
        console.debug('%c<silly>', 'color: #808080', logMessage, ...data)
        break
    }

    // if the last data is an object with logToMain: true, force log to main
    const forceLogToMain = data.length > 0 && data[data.length - 1]?.logToMain === true

    if (currentLevel >= LEVEL_MAP[this.logToMainLevel] || forceLogToMain) {
      const source: LogSourceWithContext = {
        process: 'renderer',
        window: windowSource,
        module: this.module
      }

      if (Object.keys(this.context).length > 0) {
        source.context = this.context
      }

      // remove the last item if it is an object with logToMain: true
      if (forceLogToMain) {
        data = data.slice(0, -1)
      }

      // In renderer process, use window.api.logToMain to send log to main process
      if (!IS_WORKER) {
        void window.electron.ipcRenderer.invoke(IpcChannel.App_LogToMain, source, level, message, data)
      } else {
        //TODO support worker to send log to main process
      }
    }
  }

  /**
   * Log error message
   */
  public error(message: string, ...data: LogContextData): void {
    this.processLog(LEVEL.ERROR, message, data)
  }

  /**
   * Log warning message
   */
  public warn(message: string, ...data: LogContextData): void {
    this.processLog(LEVEL.WARN, message, data)
  }

  /**
   * Log info message
   */
  public info(message: string, ...data: LogContextData): void {
    this.processLog(LEVEL.INFO, message, data)
  }

  /**
   * Log verbose message
   */
  public verbose(message: string, ...data: LogContextData): void {
    this.processLog(LEVEL.VERBOSE, message, data)
  }

  /**
   * Log debug message
   */
  public debug(message: string, ...data: LogContextData): void {
    this.processLog(LEVEL.DEBUG, message, data)
  }

  /**
   * Log silly level message
   */
  public silly(message: string, ...data: LogContextData): void {
    this.processLog(LEVEL.SILLY, message, data)
  }

  /**
   * Set the minimum log level
   * @param level - The log level to set
   */
  public setLevel(level: LogLevel): void {
    this.level = level
  }

  /**
   * Get the current log level
   * @returns The current log level
   */
  public getLevel(): string {
    return this.level
  }

  /**
   * Reset log level to environment default
   */
  public resetLevel(): void {
    this.setLevel(DEFAULT_LEVEL)
  }

  /**
   * Set the minimum level for logging to main process
   * @param level - The log level to set
   */
  public setLogToMainLevel(level: LogLevel): void {
    this.logToMainLevel = level
  }

  /**
   * Get the current log to main level
   * @returns The current log to main level
   */
  public getLogToMainLevel(): LogLevel {
    return this.logToMainLevel
  }

  /**
   * Reset log to main level to default
   */
  public resetLogToMainLevel(): void {
    this.setLogToMainLevel(MAIN_LOG_LEVEL)
  }
}

export const loggerService = new LoggerService()
