import {
  ConnectionResult,
  CreateSubscriptionArgs,
  CreateSubscriptionPeriodArgs,
  DBSubscriptionAdapter,
  DeleteSubscriptionArgs,
  DeleteSubscriptionPeriodArgs,
  GetSubscriptionArgs,
  InputCursorType,
  LimitType,
  OptionalSubscription,
  SortOrder,
  Subscription,
  SubscriptionSort,
  UpdateSubscriptionArgs
} from '@wepublish/api'

import {Collection, Db, FilterQuery, MongoCountPreferences} from 'mongodb'

import {CollectionName, DBSubscription} from './schema'
import {MaxResultsPerPage} from './defaults'
import {Cursor} from './cursor'
import nanoid from 'nanoid'
import {mapDateFilterComparisonToMongoQueryOperatior} from './utility'

export class MongoDBSubscriptionAdapter implements DBSubscriptionAdapter {
  private subscriptions: Collection<DBSubscription>
  private locale: string

  constructor(db: Db, locale: string) {
    this.subscriptions = db.collection(CollectionName.Subscriptions)
    this.locale = locale
  }

  async createSubscription({input}: CreateSubscriptionArgs): Promise<OptionalSubscription> {
    const {ops} = await this.subscriptions.insertOne({
      createdAt: new Date(),
      modifiedAt: new Date(),
      userID: input.userID,
      memberPlanID: input.memberPlanID,
      paymentMethodID: input.paymentMethodID,
      monthlyAmount: input.monthlyAmount,
      autoRenew: input.autoRenew,
      startsAt: input.startsAt,
      paymentPeriodicity: input.paymentPeriodicity,
      properties: input.properties,
      deactivation: input.deactivation,
      paidUntil: input.paidUntil,
      periods: []
    })

    const {_id: id, ...data} = ops[0]
    return {id, ...data}
  }

  async updateSubscription({id, input}: UpdateSubscriptionArgs): Promise<OptionalSubscription> {
    const {value} = await this.subscriptions.findOneAndUpdate(
      {_id: id},
      {
        $set: {
          modifiedAt: new Date(),
          userID: input.userID,
          memberPlanID: input.memberPlanID,
          paymentMethodID: input.paymentMethodID,
          monthlyAmount: input.monthlyAmount,
          autoRenew: input.autoRenew,
          startsAt: input.startsAt,
          paymentPeriodicity: input.paymentPeriodicity,
          properties: input.properties,
          deactivation: input.deactivation,
          paidUntil: input.paidUntil
        }
      },
      {returnOriginal: false}
    )

    if (!value) return null

    const {_id: outID, ...data} = value
    return {id: outID, ...data}
  }

  async updateUserID(subscriptionID: string, userID: string): Promise<OptionalSubscription> {
    const {value} = await this.subscriptions.findOneAndUpdate(
      {_id: subscriptionID},
      {
        $set: {
          modifiedAt: new Date(),
          userID
        }
      }
    )
    if (!value) return null
    return await this.subscriptions.findOne({_id: subscriptionID})
  }

  async deleteSubscription({id}: DeleteSubscriptionArgs): Promise<string | null> {
    const {deletedCount} = await this.subscriptions.deleteOne({_id: id})
    return deletedCount !== 0 ? id : null
  }

  async addSubscriptionPeriod({
    subscriptionID,
    input
  }: CreateSubscriptionPeriodArgs): Promise<OptionalSubscription> {
    const subscription = await this.subscriptions.findOne({_id: subscriptionID})
    if (!subscription) return null
    const {periods = []} = subscription

    periods.push({
      id: nanoid(),
      createdAt: new Date(),
      amount: input.amount,
      paymentPeriodicity: input.paymentPeriodicity,
      startsAt: input.startsAt,
      endsAt: input.endsAt,
      invoiceID: input.invoiceID
    })

    const {value} = await this.subscriptions.findOneAndUpdate(
      {_id: subscriptionID},
      {
        $set: {
          modifiedAt: new Date(),
          periods: periods
        }
      },
      {returnOriginal: false}
    )
    if (!value) return null

    const {_id: id, ...data} = value
    return {id, ...data}
  }

  async deleteSubscriptionPeriod({
    subscriptionID,
    periodID
  }: DeleteSubscriptionPeriodArgs): Promise<OptionalSubscription> {
    const subscription = await this.subscriptions.findOne({_id: subscriptionID})
    if (!subscription) return null
    const {periods = []} = subscription

    const updatedPeriods = periods.filter(period => period.id !== periodID)

    const {value} = await this.subscriptions.findOneAndUpdate(
      {_id: subscriptionID},
      {
        $set: {
          modifiedAt: new Date(),
          periods: updatedPeriods
        }
      },
      {returnOriginal: false}
    )

    if (!value) return null

    const {_id: id, ...data} = value
    return {id, ...data}
  }

  async getSubscriptionByID(id: string): Promise<OptionalSubscription> {
    const subscription = await this.subscriptions.findOne({_id: id})
    return subscription ? {id: subscription._id, ...subscription} : null
  }

  async getSubscriptionsByID(ids: readonly string[]): Promise<OptionalSubscription[]> {
    const subscriptions = await this.subscriptions.find({_id: {$in: ids}}).toArray()
    const subscriptionMap = Object.fromEntries(
      subscriptions.map(({_id: id, ...data}) => [id, {id, ...data}])
    )

    return ids.map(id => subscriptionMap[id] ?? null)
  }

  async getSubscriptionsByUserID(userID: string): Promise<OptionalSubscription[]> {
    const subscriptions = await this.subscriptions.find({userID: {$eq: userID}}).toArray()
    return subscriptions.map(({_id: id, ...data}) => ({id, ...data}))
  }

  async getSubscriptions({
    filter,
    joins,
    sort,
    order,
    cursor,
    limit
  }: GetSubscriptionArgs): Promise<ConnectionResult<Subscription>> {
    const limitCount = Math.min(limit.count, MaxResultsPerPage)
    const sortDirection = limit.type === LimitType.First ? order : -order
    const cursorData = cursor.type !== InputCursorType.None ? Cursor.from(cursor.data) : undefined

    const expr =
      order === SortOrder.Ascending
        ? cursor.type === InputCursorType.After
          ? '$gt'
          : '$lt'
        : cursor.type === InputCursorType.After
        ? '$lt'
        : '$gt'

    const sortField = subscriptionSortFieldForSort(sort)
    const cursorFilter = cursorData
      ? {
          $or: [
            {[sortField]: {[expr]: cursorData.date}},
            {_id: {[expr]: cursorData.id}, [sortField]: cursorData.date}
          ]
        }
      : {}

    const textFilter: FilterQuery<any> = {}
    if (filter && JSON.stringify(filter) !== '{}') {
      textFilter.$and = []
    }

    // support for old filters https://github.com/wepublish/wepublish/issues/601 -->
    if (filter?.startsAt !== undefined) {
      const {comparison, date} = filter.startsAt
      textFilter.$and?.push({
        startsAt: {[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date}
      })
    }
    if (filter?.paidUntil !== undefined) {
      const {comparison, date} = filter.paidUntil
      textFilter.$and?.push({
        paidUntil: {
          [mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
        }
      })
    }
    // <-- support for old filters

    if (filter?.startsAtFrom) {
      const {comparison, date} = filter.startsAtFrom
      textFilter.$and?.push({
        startsAt: {[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date}
      })
    }

    if (filter?.startsAtTo) {
      const {comparison, date} = filter.startsAtTo
      textFilter.$and?.push({
        startsAt: {[mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date}
      })
    }

    if (filter?.paidUntilFrom) {
      const {comparison, date} = filter.paidUntilFrom
      textFilter.$and?.push({
        paidUntil: {
          [mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
        }
      })
    }

    if (filter?.paidUntilTo) {
      const {comparison, date} = filter.paidUntilTo
      textFilter.$and?.push({
        paidUntil: {
          [mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
        }
      })
    }

    if (filter?.deactivationDate !== undefined) {
      const {comparison, date} = filter.deactivationDate

      if (date === null) {
        textFilter.$and?.push({deactivation: {$eq: null}})
      } else {
        textFilter.$and?.push({
          'deactivation.date': {
            [mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
          }
        })
      }
    }

    if (filter?.deactivationDateFrom !== undefined) {
      const {comparison, date} = filter.deactivationDateFrom

      textFilter.$and?.push({
        'deactivation.date': {
          [mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
        }
      })
    }

    if (filter?.deactivationDateTo !== undefined) {
      const {comparison, date} = filter.deactivationDateTo

      textFilter.$and?.push({
        'deactivation.date': {
          [mapDateFilterComparisonToMongoQueryOperatior(comparison)]: date
        }
      })
    }

    if (filter?.deactivationReason !== undefined) {
      const reason = filter.deactivationReason

      textFilter.$and?.push({
        'deactivation.reason': reason
      })
    }

    if (filter?.autoRenew !== undefined) {
      textFilter.$and?.push({autoRenew: {$eq: filter.autoRenew}})
    }

    if (filter?.paymentPeriodicity) {
      textFilter.$and?.push({paymentPeriodicity: {$eq: filter.paymentPeriodicity}})
    }

    if (filter?.paymentMethodID) {
      textFilter.$and?.push({paymentMethodID: {$eq: filter.paymentMethodID}})
    }

    if (filter?.memberPlanID) {
      textFilter.$and?.push({memberPlanID: {$eq: filter.memberPlanID}})
    }

    if (filter?.userID) {
      textFilter.$and?.push({userID: {$eq: filter.userID}})
    }

    // join related collections
    let preparedJoins: any = []
    // member plan join
    if (joins?.joinMemberPlan) {
      preparedJoins = [
        {
          $lookup: {
            from: CollectionName.MemberPlans,
            localField: 'memberPlanID',
            foreignField: '_id',
            as: 'memberPlan'
          }
        },
        {$unwind: '$memberPlan'}
      ]
    }
    // payment method join
    if (joins?.joinPaymentMethod) {
      preparedJoins = [
        ...preparedJoins,
        {
          $lookup: {
            from: CollectionName.PaymentMethods,
            localField: 'paymentMethodID',
            foreignField: '_id',
            as: 'paymentMethod'
          }
        },
        {$unwind: '$paymentMethod'}
      ]
    }
    // user join
    if (joins?.joinUser) {
      preparedJoins = [
        ...preparedJoins,
        {
          $lookup: {
            from: CollectionName.Users,
            localField: 'userID',
            foreignField: '_id',
            as: 'user'
          }
        },
        {$unwind: '$user'}
      ]
    }

    const [totalCount, subscriptions] = await Promise.all([
      this.subscriptions.countDocuments(textFilter, {
        collation: {locale: this.locale, strength: 2}
      } as MongoCountPreferences), // MongoCountPreferences doesn't include collation

      this.subscriptions
        .aggregate([...preparedJoins], {collation: {locale: this.locale, strength: 2}})
        .match(textFilter)
        .match(cursorFilter)
        .sort({[sortField]: sortDirection, _id: sortDirection})
        .skip(limit.skip ?? 0)
        .limit(limitCount + 1)
        .toArray()
    ])

    const nodes = subscriptions.slice(0, limitCount)

    if (limit.type === LimitType.Last) {
      nodes.reverse()
    }

    const hasNextPage =
      limit.type === LimitType.First
        ? subscriptions.length > limitCount
        : cursor.type === InputCursorType.Before

    const hasPreviousPage =
      limit.type === LimitType.Last
        ? subscriptions.length > limitCount
        : cursor.type === InputCursorType.After

    const firstUser = nodes[0]
    const lastUser = nodes[nodes.length - 1]

    const startCursor = firstUser
      ? new Cursor(firstUser._id, subscriptionDateForSort(firstUser, sort)).toString()
      : null

    const endCursor = lastUser
      ? new Cursor(lastUser._id, subscriptionDateForSort(lastUser, sort)).toString()
      : null

    return {
      nodes: nodes.map<Subscription>(({_id: id, ...data}) => ({id, ...data})),

      pageInfo: {
        startCursor,
        endCursor,
        hasNextPage,
        hasPreviousPage
      },

      totalCount
    }
  }
}

function subscriptionSortFieldForSort(sort: SubscriptionSort) {
  switch (sort) {
    case SubscriptionSort.CreatedAt:
      return 'createdAt'

    case SubscriptionSort.ModifiedAt:
      return 'modifiedAt'
  }
}

function subscriptionDateForSort(subscription: DBSubscription, sort: SubscriptionSort): Date {
  switch (sort) {
    case SubscriptionSort.CreatedAt:
      return subscription.createdAt

    case SubscriptionSort.ModifiedAt:
      return subscription.modifiedAt
  }
}
