// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only

import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { ChannelConfigurationOptions } from '../../../../shared/lib/types'
import type { ChannelCommonRoomConf } from '../../configuration/bot'
import { RoomChannel } from '../../room-channel'
import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize'
import {
  forbidSpecialCharsDefaultTolerance,
  noDuplicateDefaultDelay
} from '../../../../shared/lib/constants'
import * as fs from 'fs'
import * as path from 'path'

// FIXME: should be exported by xmppjs-chat-bot
type ConfigHandlers = ChannelCommonRoomConf['handlers']
type ConfigHandler = ConfigHandlers[0]

/**
 * Get saved configuration options for the given channel.
 * Can throw an exception.
 * @param options Peertube server options
 * @param channelInfos Info from channel from which we want to get infos
 * @returns Channel configuration data, or null if nothing is stored
 */
async function getChannelConfigurationOptions (
  options: RegisterServerOptions,
  channelId: number | string
): Promise<ChannelConfigurationOptions | null> {
  const logger = options.peertubeHelpers.logger
  const filePath = _getFilePath(options, channelId)
  if (!fs.existsSync(filePath)) {
    logger.debug('No stored data for channel, returning null')
    return null
  }
  const content = await fs.promises.readFile(filePath, {
    encoding: 'utf-8'
  })
  const sanitized = await sanitizeChannelConfigurationOptions(options, channelId, JSON.parse(content))
  return sanitized
}

function getDefaultChannelConfigurationOptions (_options: RegisterServerOptions): ChannelConfigurationOptions {
  return {
    bot: {
      enabled: false,
      nickname: 'Sepia',
      forbiddenWords: [],
      forbidSpecialChars: {
        enabled: false,
        reason: '',
        tolerance: forbidSpecialCharsDefaultTolerance,
        applyToModerators: false
      },
      noDuplicate: {
        enabled: false,
        reason: '',
        delay: noDuplicateDefaultDelay,
        applyToModerators: false
      },
      quotes: [],
      commands: []
    },
    slowMode: {
      duration: 0
    },
    mute: {
      anonymous: false
    },
    moderation: {
      delay: 0,
      anonymize: false
    },
    terms: undefined
  }
}

/**
 * Save channel configuration options.
 * Can throw an exception.
 * @param options Peertube server options
 * @param ChannelConfigurationOptions data to save
 */
async function storeChannelConfigurationOptions (
  options: RegisterServerOptions,
  channelId: number | string,
  channelConfigurationOptions: ChannelConfigurationOptions
): Promise<void> {
  const filePath = _getFilePath(options, channelId)

  if (!fs.existsSync(filePath)) {
    const dir = path.dirname(filePath)
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true })
    }
  }

  const jsonContent = JSON.stringify(channelConfigurationOptions)

  await fs.promises.writeFile(filePath, jsonContent, {
    encoding: 'utf-8'
  })

  RoomChannel.singleton().refreshChannelConfigurationOptions(channelId)
}

/**
 * Converts the channel configuration to the bot room configuration object (minus the room JID and domain)
 * @param options server options
 * @param channelConfigurationOptions The channel configuration
 * @param previousRoomConf the previous saved room conf, if available. Used to merge handlers.
 * @returns Partial bot room configuration
 */
function channelConfigurationOptionsToBotRoomConf (
  options: RegisterServerOptions,
  channelConfigurationOptions: ChannelConfigurationOptions,
  previousRoomConf: ChannelCommonRoomConf | null
): ChannelCommonRoomConf {
  // Note concerning handlers:
  // If we want the bot to correctly enable/disable the handlers,
  // we must always define all handlers, even if not used.
  // That's why we are gathering handlers ids in handlersId, and disabling missing handlers at the end of this function.
  const handlersIds: Map<string, true> = new Map<string, true>()
  const handlers: ConfigHandlers = []
  channelConfigurationOptions.bot.forbiddenWords.forEach((v, i) => {
    const id = 'forbidden_words_' + i.toString()
    handlersIds.set(id, true)
    handlers.push(_getForbiddenWordsHandler(id, v))
  })
  if (channelConfigurationOptions.bot.forbidSpecialChars.enabled) {
    const id = 'forbid_special_chars'
    handlersIds.set(id, true)
    handlers.push(_getForbidSpecialCharsHandler(id, channelConfigurationOptions.bot.forbidSpecialChars))
  }
  if (channelConfigurationOptions.bot.noDuplicate.enabled) {
    const id = 'no_duplicate'
    handlersIds.set(id, true)
    handlers.push(_getNoDuplicateHandler(id, channelConfigurationOptions.bot.noDuplicate))
  }
  channelConfigurationOptions.bot.quotes.forEach((v, i) => {
    const id = 'quote_' + i.toString()
    handlersIds.set(id, true)
    handlers.push(_getQuotesHandler(id, v))
  })
  channelConfigurationOptions.bot.commands.forEach((v, i) => {
    const id = 'command_' + i.toString()
    handlersIds.set(id, true)
    handlers.push(_getCommandsHandler(id, v))
  })

  // Disabling missing handlers:
  if (previousRoomConf) {
    for (const handler of previousRoomConf.handlers) {
      if (!handlersIds.has(handler.id)) {
        // cloning to avoid issues...
        const disabledHandler = JSON.parse(JSON.stringify(handler)) as typeof handler
        disabledHandler.enabled = false
        handlers.push(disabledHandler)
      }
    }
  }

  const roomConf: ChannelCommonRoomConf = {
    enabled: channelConfigurationOptions.bot.enabled,
    handlers
  }
  if (channelConfigurationOptions.bot.nickname && channelConfigurationOptions.bot.nickname !== '') {
    roomConf.nick = channelConfigurationOptions.bot.nickname
  }
  return roomConf
}

function _getForbiddenWordsHandler (
  id: string,
  forbiddenWords: ChannelConfigurationOptions['bot']['forbiddenWords'][0]
): ConfigHandler {
  const handler: ConfigHandler = {
    type: 'moderate',
    id,
    enabled: false,
    options: {
      rules: []
    }
  }
  if (forbiddenWords.entries.length === 0) {
    return handler
  }

  handler.enabled = forbiddenWords.enabled
  const rule: any = {
    name: id
  }

  if (forbiddenWords.regexp) {
    // Note: on the Peertube frontend, channelConfigurationOptions.forbiddenWords
    // is an array of RegExp definition (strings).
    // They are validated one by bone.
    // To increase the bot performance, we will join them all (hopping the bot will optimize them).
    rule.regexp = '(?:' + forbiddenWords.entries.join(')|(?:') + ')'
  } else {
    // Here we must add word-breaks and escape entries.
    // We join all entries in one Regexp (for the same reason as above).
    rule.regexp = '(?:' + forbiddenWords.entries.map(
      s => {
        s = _stringToWordRegexp(s)
        // Must add the \b...
        // ... but... won't work if the first (or last) char is an emoji.
        // So, doing this trick:
        if (/^\w/.test(s)) {
          s = '\\b' + s
        }
        if (/\w$/.test(s)) {
          s = s + '\\b'
        }
        // FIXME: this solution wont work for non-latin charsets.
        return s
      }
    ).join(')|(?:') + ')'
  }

  if (forbiddenWords.reason) {
    rule.reason = forbiddenWords.reason
  }
  handler.options.rules.push(rule)

  handler.options.applyToModerators = !!forbiddenWords.applyToModerators
  return handler
}

function _getForbidSpecialCharsHandler (
  id: string,
  forbidSpecialChars: ChannelConfigurationOptions['bot']['forbidSpecialChars']
): ConfigHandler {
  const handler: ConfigHandler = {
    type: 'moderate',
    id,
    enabled: true,
    options: {
      rules: []
    }
  }

  // The regexp to find one invalid character:
  // (Note: Emoji_Modifier and Emoji_Component should not be matched alones, but seems a reasonnable compromise to avoid
  // complex regex).
  let regexp = '[^' +
    '\\s\\p{Letter}\\p{Number}\\p{Punctuation}\\p{Currency_Symbol}\\p{Emoji}\\p{Emoji_Component}\\p{Emoji_Modifier}' +
    ']'

  if (forbidSpecialChars.tolerance > 0) {
    // we must repeat !
    const a = []
    for (let i = 0; i <= forbidSpecialChars.tolerance; i++) { // N+1 values
      a.push(regexp)
    }
    regexp = a.join('.*')
  }

  const rule: any = {
    name: id,
    regexp,
    modifiers: 'us',
    reason: forbidSpecialChars.reason
  }
  handler.options.rules.push(rule)
  handler.options.applyToModerators = !!forbidSpecialChars.applyToModerators
  return handler
}

function _getNoDuplicateHandler (
  id: string,
  noDuplicate: ChannelConfigurationOptions['bot']['noDuplicate']
): ConfigHandler {
  const handler: ConfigHandler = {
    type: 'no-duplicate',
    id,
    enabled: true,
    options: {
      reason: noDuplicate.reason,
      delay: noDuplicate.delay,
      applyToModerators: !!noDuplicate.applyToModerators
    }
  }
  return handler
}

function _getQuotesHandler (
  id: string,
  quotes: ChannelConfigurationOptions['bot']['quotes'][0]
): ConfigHandler {
  const handler: ConfigHandler = {
    type: 'quotes_random',
    id,
    enabled: false,
    options: {
      quotes: [],
      delay: 5 * 60
    }
  }
  if (quotes.messages.length === 0) {
    return handler
  }

  handler.enabled = true
  handler.options.quotes = quotes.messages
  handler.options.delay = quotes.delay
  return handler
}

function _getCommandsHandler (
  id: string,
  command: ChannelConfigurationOptions['bot']['commands'][0]
): ConfigHandler {
  const handler: ConfigHandler = {
    type: 'command_say',
    id,
    enabled: false,
    options: {
      quotes: [],
      command: 'undefined' // This is arbitrary, and does not matter as enabled=false
    }
  }
  if (!command.message || command.message === '') {
    return handler
  }

  handler.enabled = true
  handler.options.command = command.command
  handler.options.quotes = [command.message]
  return handler
}

const stringToWordRegexpSpecials = [
  // order matters for these
  '-', '[', ']',
  // order doesn't matter for any of these
  '/', '{', '}', '(', ')', '*', '+', '?', '.', '\\', '^', '$', '|'
]
// I choose to escape every character with '\'
// even though only some strictly require it when inside of []
const stringToWordRegexp = RegExp('[' + stringToWordRegexpSpecials.join('\\') + ']', 'g')
function _stringToWordRegexp (s: string): string {
  return s.replace(stringToWordRegexp, '\\$&')
}

function _getFilePath (
  options: RegisterServerOptions,
  channelId: number | string
): string {
  // some sanitization, just in case...
  channelId = parseInt(channelId.toString())
  if (isNaN(channelId)) {
    throw new Error(`Invalid channelId: ${channelId}`)
  }

  return path.resolve(
    options.peertubeHelpers.plugin.getDataDirectoryPath(),
    'channelConfigurationOptions',
    channelId.toString() + '.json'
  )
}

export {
  getChannelConfigurationOptions,
  getDefaultChannelConfigurationOptions,
  channelConfigurationOptionsToBotRoomConf,
  storeChannelConfigurationOptions
}
