import { Op, Transaction } from 'sequelize'
import { sequelizeTypescript } from '@server/initializers/database'
import { AccountModel } from '@server/models/account/account'
import { ActorModel } from '@server/models/actor/actor'
import { ServerModel } from '@server/models/server/server'
import { VideoChannelModel } from '@server/models/video/video-channel'
import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models'
import { ActivityPubActor, ActorImageType } from '@shared/models'
import { updateActorImages } from '../image'
import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes'
import { fetchActorFollowsCount } from './url-to-object'

export class APActorCreator {

  constructor (
    private readonly actorObject: ActivityPubActor,
    private readonly ownerActor?: MActorFullActor
  ) {

  }

  async create (): Promise<MActorFullActor> {
    const { followersCount, followingCount } = await fetchActorFollowsCount(this.actorObject)

    const actorInstance = new ActorModel(getActorAttributesFromObject(this.actorObject, followersCount, followingCount))

    return sequelizeTypescript.transaction(async t => {
      const server = await this.setServer(actorInstance, t)

      const { actorCreated, created } = await this.saveActor(actorInstance, t)

      await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t)
      await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, t)

      await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t)

      if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance
        actorCreated.Account = await this.saveAccount(actorCreated, t) as MAccountDefault
        actorCreated.Account.Actor = actorCreated
      }

      if (actorCreated.type === 'Group') { // Video channel
        const channel = await this.saveVideoChannel(actorCreated, t)
        actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: this.ownerActor.Account })
      }

      actorCreated.Server = server

      return actorCreated
    })
  }

  private async setServer (actor: MActor, t: Transaction) {
    const actorHost = new URL(actor.url).host

    const serverOptions = {
      where: {
        host: actorHost
      },
      defaults: {
        host: actorHost
      },
      transaction: t
    }
    const [ server ] = await ServerModel.findOrCreate(serverOptions)

    // Save our new account in database
    actor.serverId = server.id

    return server as MServer
  }

  private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) {
    const imagesInfo = getImagesInfoFromObject(this.actorObject, type)
    if (imagesInfo.length === 0) return

    return updateActorImages(actor as MActorImages, type, imagesInfo, t)
  }

  private async saveActor (actor: MActor, t: Transaction) {
    // Force the actor creation using findOrCreate() instead of save()
    // Sometimes Sequelize skips the save() when it thinks the instance already exists
    // (which could be false in a retried query)
    const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
      defaults: actor.toJSON(),
      where: {
        [Op.or]: [
          {
            url: actor.url
          },
          {
            serverId: actor.serverId,
            preferredUsername: actor.preferredUsername
          }
        ]
      },
      transaction: t
    })

    return { actorCreated, created }
  }

  private async tryToFixActorUrlIfNeeded (actorCreated: MActor, newActor: MActor, created: boolean, t: Transaction) {
    // Try to fix non HTTPS accounts of remote instances that fixed their URL afterwards
    if (created !== true && actorCreated.url !== newActor.url) {
      // Only fix http://example.com/account/djidane to https://example.com/account/djidane
      if (actorCreated.url.replace(/^http:\/\//, '') !== newActor.url.replace(/^https:\/\//, '')) {
        throw new Error(`Actor from DB with URL ${actorCreated.url} does not correspond to actor ${newActor.url}`)
      }

      actorCreated.url = newActor.url
      await actorCreated.save({ transaction: t })
    }
  }

  private async saveAccount (actor: MActorId, t: Transaction) {
    const [ accountCreated ] = await AccountModel.findOrCreate({
      defaults: {
        name: getActorDisplayNameFromObject(this.actorObject),
        description: this.actorObject.summary,
        actorId: actor.id
      },
      where: {
        actorId: actor.id
      },
      transaction: t
    })

    return accountCreated as MAccount
  }

  private async saveVideoChannel (actor: MActorId, t: Transaction) {
    const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
      defaults: {
        name: getActorDisplayNameFromObject(this.actorObject),
        description: this.actorObject.summary,
        support: this.actorObject.support,
        actorId: actor.id,
        accountId: this.ownerActor.Account.id
      },
      where: {
        actorId: actor.id
      },
      transaction: t
    })

    return videoChannelCreated as MChannel
  }
}
