import { VideoImport, type VideoImportPayload, VideoImportState, type VideoImportStateType } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js'
import { MVideoImport, MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import.js'
import { Op } from 'sequelize'
import {
  AllowNull,
  BelongsTo,
  Column,
  CreatedAt,
  DataType,
  Default,
  DefaultScope,
  ForeignKey,
  Is,
  Table,
  UpdatedAt
} from 'sequelize-typescript'
import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports.js'
import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos.js'
import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants.js'
import { buildSQLAttributes, getSort, SequelizeModel, throwIfNotValid } from '../shared/index.js'
import { UserModel } from '../user/user.js'
import { ListVideoImportsOptions, VideoImportListQueryBuilder } from './sql/import/video-import-list-query-builder.js'
import { VideoChannelSyncModel } from './video-channel-sync.js'
import { VideoModel, ScopeNames as VideoModelScopeNames } from './video.js'

const defaultVideoScope = () => {
  return VideoModel.scope([
    VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
    VideoModelScopeNames.WITH_TAGS,
    VideoModelScopeNames.WITH_THUMBNAILS
  ])
}

@DefaultScope(() => ({
  include: [
    {
      model: UserModel.unscoped(),
      required: true
    },
    {
      model: defaultVideoScope(),
      required: false
    },
    {
      model: VideoChannelSyncModel.unscoped(),
      required: false
    }
  ]
}))
@Table({
  tableName: 'videoImport',
  indexes: [
    {
      fields: [ 'videoId' ],
      unique: true
    },
    {
      fields: [ 'userId' ]
    }
  ]
})
export class VideoImportModel extends SequelizeModel<VideoImportModel> {
  @CreatedAt
  declare createdAt: Date

  @UpdatedAt
  declare updatedAt: Date

  @AllowNull(true)
  @Default(null)
  @Is('VideoImportTargetUrl', value => throwIfNotValid(value, isVideoImportTargetUrlValid, 'targetUrl', true))
  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max))
  declare targetUrl: string

  @AllowNull(true)
  @Default(null)
  @Is('VideoImportMagnetUri', value => throwIfNotValid(value, isVideoMagnetUriValid, 'magnetUri', true))
  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max)) // Use the same constraints than URLs
  declare magnetUri: string

  @AllowNull(true)
  @Default(null)
  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_NAME.max))
  declare torrentName: string

  @AllowNull(false)
  @Default(null)
  @Is('VideoImportState', value => throwIfNotValid(value, isVideoImportStateValid, 'state'))
  @Column
  declare state: VideoImportStateType

  @AllowNull(true)
  @Default(null)
  @Column(DataType.TEXT)
  declare error: string

  @AllowNull(false)
  @Default(0)
  @Column
  declare attempts: number

  @AllowNull(true)
  @Column(DataType.JSONB)
  declare payload: VideoImportPayload

  @ForeignKey(() => UserModel)
  @Column
  declare userId: number

  @BelongsTo(() => UserModel, {
    foreignKey: {
      allowNull: false
    },
    onDelete: 'cascade'
  })
  declare User: Awaited<UserModel>

  @ForeignKey(() => VideoModel)
  @Column
  declare videoId: number

  @BelongsTo(() => VideoModel, {
    foreignKey: {
      allowNull: true
    },
    onDelete: 'set null'
  })
  declare Video: Awaited<VideoModel>

  @ForeignKey(() => VideoChannelSyncModel)
  @Column
  declare videoChannelSyncId: number

  @BelongsTo(() => VideoChannelSyncModel, {
    foreignKey: {
      allowNull: true
    },
    onDelete: 'set null'
  })
  declare VideoChannelSync: Awaited<VideoChannelSyncModel>

  // ---------------------------------------------------------------------------

  static getSQLAttributes (tableName: string, aliasPrefix = '') {
    return buildSQLAttributes({
      model: this,
      tableName,
      aliasPrefix
    })
  }

  // ---------------------------------------------------------------------------

  static loadAndPopulateVideo (id: number): Promise<MVideoImportDefault> {
    return VideoImportModel.findByPk(id)
  }

  static listUserVideoImportsForApi (options: ListVideoImportsOptions) {
    return Promise.all([
      new VideoImportListQueryBuilder(VideoImportModel.sequelize, options).list<MVideoImportFormattable>(),
      new VideoImportListQueryBuilder(VideoImportModel.sequelize, options).count()
    ]).then(([ rows, count ]) => {
      return { total: count, data: rows }
    })
  }

  static async urlAlreadyImported (options: {
    targetUrl: string
    channelId: number
    channelSyncId?: number
  }): Promise<boolean> {
    const { channelSyncId, channelId, targetUrl } = options

    const baseWhere = { targetUrl }

    const bySyncId = channelSyncId
      ? VideoImportModel.unscoped().findOne({
        where: {
          ...baseWhere,

          videoChannelSyncId: channelSyncId
        }
      })
      : Promise.resolve(undefined)

    const byChannelId = VideoImportModel.unscoped().findOne({
      where: baseWhere,
      include: [
        {
          model: VideoModel.unscoped(),
          required: true,
          where: {
            channelId
          }
        }
      ]
    })

    return (await Promise.all([ bySyncId, byChannelId ])).some(e => !!e)
  }

  static listFailedBySyncId (options: {
    channelSyncId?: number
  }): Promise<MVideoImport[]> {
    return VideoImportModel.unscoped().findAll({
      include: [
        {
          model: VideoModel.unscoped(),
          required: true
        }
      ],
      where: {
        videoChannelSyncId: options.channelSyncId,
        state: {
          [Op.in]: [ VideoImportState.FAILED ]
        },
        attempts: {
          [Op.lt]: CONFIG.IMPORT.VIDEOS.MAX_ATTEMPTS
        }
      },
      limit: 100
    })
  }

  static loadLastImportBySyncId (options: {
    channelSyncId: number
  }) {
    return VideoImportModel.findOne<MVideoImportDefault>({
      where: {
        videoChannelSyncId: options.channelSyncId
      },
      order: getSort('-createdAt')
    })
  }

  // ---------------------------------------------------------------------------

  getTargetIdentifier () {
    return this.targetUrl || this.magnetUri || this.torrentName
  }

  toFormattedJSON (this: MVideoImportFormattable): VideoImport {
    const videoFormatOptions = {
      completeDescription: true,
      additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true }
    }
    const video = this.Video
      ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) })
      : undefined

    const videoChannelSync = this.VideoChannelSync
      ? { id: this.VideoChannelSync.id, externalChannelUrl: this.VideoChannelSync.externalChannelUrl }
      : undefined

    return {
      id: this.id,

      targetUrl: this.targetUrl,
      magnetUri: this.magnetUri,
      torrentName: this.torrentName,

      attempts: this.attempts,

      state: {
        id: this.state,
        label: VideoImportModel.getStateLabel(this.state)
      },
      error: this.error,
      updatedAt: this.updatedAt.toISOString(),
      createdAt: this.createdAt.toISOString(),
      video,
      videoChannelSync
    }
  }

  private static getStateLabel (id: number) {
    return VIDEO_IMPORT_STATES[id] || 'Unknown'
  }
}
