import { FindAndCountOptions, json, QueryTypes } from 'sequelize'
import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { MPlugin, MPluginFormattable } from '@server/types/models'
import { AttributesOnly } from '@shared/typescript-utils'
import { PeerTubePlugin, PluginType, RegisterServerSettingOptions, SettingEntries, SettingValue } from '../../../shared/models'
import {
  isPluginDescriptionValid,
  isPluginHomepage,
  isPluginNameValid,
  isPluginStableOrUnstableVersionValid,
  isPluginStableVersionValid,
  isPluginTypeValid
} from '../../helpers/custom-validators/plugins'
import { getSort, throwIfNotValid } from '../shared'

@DefaultScope(() => ({
  attributes: {
    exclude: [ 'storage' ]
  }
}))

@Table({
  tableName: 'plugin',
  indexes: [
    {
      fields: [ 'name', 'type' ],
      unique: true
    }
  ]
})
export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> {

  @AllowNull(false)
  @Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name'))
  @Column
  name: string

  @AllowNull(false)
  @Is('PluginType', value => throwIfNotValid(value, isPluginTypeValid, 'type'))
  @Column
  type: number

  @AllowNull(false)
  @Is('PluginVersion', value => throwIfNotValid(value, isPluginStableOrUnstableVersionValid, 'version'))
  @Column
  version: string

  @AllowNull(true)
  @Is('PluginLatestVersion', value => throwIfNotValid(value, isPluginStableVersionValid, 'version'))
  @Column
  latestVersion: string

  @AllowNull(false)
  @Column
  enabled: boolean

  @AllowNull(false)
  @Column
  uninstalled: boolean

  @AllowNull(false)
  @Column
  peertubeEngine: string

  @AllowNull(true)
  @Is('PluginDescription', value => throwIfNotValid(value, isPluginDescriptionValid, 'description'))
  @Column
  description: string

  @AllowNull(false)
  @Is('PluginHomepage', value => throwIfNotValid(value, isPluginHomepage, 'homepage'))
  @Column
  homepage: string

  @AllowNull(true)
  @Column(DataType.JSONB)
  settings: any

  @AllowNull(true)
  @Column(DataType.JSONB)
  storage: any

  @CreatedAt
  createdAt: Date

  @UpdatedAt
  updatedAt: Date

  static listEnabledPluginsAndThemes (): Promise<MPlugin[]> {
    const query = {
      where: {
        enabled: true,
        uninstalled: false
      }
    }

    return PluginModel.findAll(query)
  }

  static loadByNpmName (npmName: string): Promise<MPlugin> {
    const name = this.normalizePluginName(npmName)
    const type = this.getTypeFromNpmName(npmName)

    const query = {
      where: {
        name,
        type
      }
    }

    return PluginModel.findOne(query)
  }

  static getSetting (pluginName: string, pluginType: PluginType, settingName: string, registeredSettings: RegisterServerSettingOptions[]) {
    const query = {
      attributes: [ 'settings' ],
      where: {
        name: pluginName,
        type: pluginType
      }
    }

    return PluginModel.findOne(query)
      .then(p => {
        if (!p?.settings || p.settings === undefined) {
          const registered = registeredSettings.find(s => s.name === settingName)
          if (!registered || registered.default === undefined) return undefined

          return registered.default
        }

        return p.settings[settingName]
      })
  }

  static getSettings (
    pluginName: string,
    pluginType: PluginType,
    settingNames: string[],
    registeredSettings: RegisterServerSettingOptions[]
  ) {
    const query = {
      attributes: [ 'settings' ],
      where: {
        name: pluginName,
        type: pluginType
      }
    }

    return PluginModel.findOne(query)
      .then(p => {
        const result: SettingEntries = {}

        for (const name of settingNames) {
          if (!p?.settings || p.settings[name] === undefined) {
            const registered = registeredSettings.find(s => s.name === name)

            if (registered?.default !== undefined) {
              result[name] = registered.default
            }
          } else {
            result[name] = p.settings[name]
          }
        }

        return result
      })
  }

  static setSetting (pluginName: string, pluginType: PluginType, settingName: string, settingValue: SettingValue) {
    const query = {
      where: {
        name: pluginName,
        type: pluginType
      }
    }

    const toSave = {
      [`settings.${settingName}`]: settingValue
    }

    return PluginModel.update(toSave, query)
      .then(() => undefined)
  }

  static getData (pluginName: string, pluginType: PluginType, key: string) {
    const query = {
      raw: true,
      attributes: [ [ json('storage.' + key), 'value' ] as any ], // FIXME: typings
      where: {
        name: pluginName,
        type: pluginType
      }
    }

    return PluginModel.findOne(query)
      .then((c: any) => {
        if (!c) return undefined
        const value = c.value

        try {
          return JSON.parse(value)
        } catch {
          return value
        }
      })
  }

  static storeData (pluginName: string, pluginType: PluginType, key: string, data: any) {
    const query = 'UPDATE "plugin" SET "storage" = jsonb_set(coalesce("storage", \'{}\'), :key, :data::jsonb) ' +
    'WHERE "name" = :pluginName AND "type" = :pluginType'

    const jsonPath = '{' + key + '}'

    const options = {
      replacements: { pluginName, pluginType, key: jsonPath, data: JSON.stringify(data) },
      type: QueryTypes.UPDATE
    }

    return PluginModel.sequelize.query(query, options)
                      .then(() => undefined)
  }

  static listForApi (options: {
    pluginType?: PluginType
    uninstalled?: boolean
    start: number
    count: number
    sort: string
  }) {
    const { uninstalled = false } = options
    const query: FindAndCountOptions = {
      offset: options.start,
      limit: options.count,
      order: getSort(options.sort),
      where: {
        uninstalled
      }
    }

    if (options.pluginType) query.where['type'] = options.pluginType

    return Promise.all([
      PluginModel.count(query),
      PluginModel.findAll<MPlugin>(query)
    ]).then(([ total, data ]) => ({ total, data }))
  }

  static listInstalled (): Promise<MPlugin[]> {
    const query = {
      where: {
        uninstalled: false
      }
    }

    return PluginModel.findAll(query)
  }

  static normalizePluginName (npmName: string) {
    return npmName.replace(/^peertube-((theme)|(plugin))-/, '')
  }

  static getTypeFromNpmName (npmName: string) {
    return npmName.startsWith('peertube-plugin-')
      ? PluginType.PLUGIN
      : PluginType.THEME
  }

  static buildNpmName (name: string, type: PluginType) {
    if (type === PluginType.THEME) return 'peertube-theme-' + name

    return 'peertube-plugin-' + name
  }

  getPublicSettings (registeredSettings: RegisterServerSettingOptions[]) {
    const result: SettingEntries = {}
    const settings = this.settings || {}

    for (const r of registeredSettings) {
      if (r.private !== false) continue

      result[r.name] = settings[r.name] ?? r.default ?? null
    }

    return result
  }

  toFormattedJSON (this: MPluginFormattable): PeerTubePlugin {
    return {
      name: this.name,
      type: this.type,
      version: this.version,
      latestVersion: this.latestVersion,
      enabled: this.enabled,
      uninstalled: this.uninstalled,
      peertubeEngine: this.peertubeEngine,
      description: this.description,
      homepage: this.homepage,
      settings: this.settings,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt
    }
  }

}
