import uniq from 'lodash/uniq'

import {
  Template,
  EmailCampaigns,
  CampaignTemplates,
  MailDomain,
  PaginationDirection,
  EmailCampaignSendType,
  EmailCampaignSchema,
  ResponseError
} from '@komi-app/messaging-sdk'
import notification from 'utils/notification'

import { Response } from 'redux/Common/types'
import {
  CountQueryParameters,
  PaginationQueryParameters
} from 'hooks/usePagination'
import { CountQueryResponse } from 'types/query'

import {
  CampaignReportResponse,
  CampaignResponse,
  CampaignStatus,
  CampaignsWithCursorResponse,
  CampaignUpsertDto,
  CheckNameExistsResponse,
  SendType
} from 'types/campaigns'
import {
  CampaignTemplateCreateDto,
  CampaignTemplateResponse,
  TemplateResponse,
  TemplatesWithCursorResponse
} from 'types/templates'
import {
  PatchEmailDomainRequest,
  PostEmailDomainRequest,
  GetEmailDomainResponse,
  PostSendTestEmailRequest
} from 'types/email-domains'
import { DeepPartial } from 'types/utilities'
import config from 'config'
import { getSegmentServiceV1 } from 'services'
import { GeneralApiProblem } from './apiProblem'

const messagingConfig = {
  targetUrl: config.messaging.url,
  serviceName: config.service.name,
  serviceVersion: config.service.version,
  authUrl: config.auth.url
}

const messagingServiceDefaultErrorMessage =
  'Oops, we are currently experiencing some unexpected problems. Please come back to the site a bit later on'

export class MessagingAdaptor {
  constructor() {
    this.templates = new Template(messagingConfig)
    this.campaigns = new EmailCampaigns(messagingConfig)
    this.campaignTemplates = new CampaignTemplates(messagingConfig)
    this.mailDomain = new MailDomain(messagingConfig)
  }

  private templates: Template
  private campaigns: EmailCampaigns
  private campaignTemplates: CampaignTemplates
  private mailDomain: MailDomain

  init(profileId: string) {
    this.templates.init(profileId)
    this.campaigns.init(profileId)
    this.campaignTemplates.init(profileId)
    this.mailDomain.init(profileId)
  }

  // Templates
  async getTemplate(templateId: string): Promise<Response<TemplateResponse>> {
    try {
      return {
        ok: true,
        response: {
          template: await this.templates.getTemplate(templateId)
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async getTemplates(
    queryParams: PaginationQueryParameters,
    searchTerm: string
  ): Promise<Response<TemplatesWithCursorResponse>> {
    try {
      return {
        ok: true,
        response: {
          templates: await this.templates.getTemplates({
            ...queryParams,
            direction: PaginationDirection[queryParams.direction],
            searchTerm
          })
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async getTemplatesCount(
    countParams: CountQueryParameters,
    searchTerm: string
  ): Promise<Response<CountQueryResponse>> {
    try {
      return {
        ok: true,
        response: {
          count: await this.templates.getTemplateCount({
            ...countParams,
            searchTerm
          })
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async updateTemplate(
    templateId: string,
    payload: CampaignTemplateCreateDto
  ): Promise<Response<undefined>> {
    try {
      await this.templates.updateTemplate(templateId, payload)

      return {
        ok: true,
        response: undefined
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async createTemplate(
    payload: CampaignTemplateCreateDto
  ): Promise<Response<TemplateResponse>> {
    try {
      return {
        ok: true,
        response: {
          template: await this.templates.createTemplate(payload)
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async deleteTemplate(templateId: string): Promise<Response<boolean>> {
    try {
      await this.templates.deleteTemplate(templateId)

      return {
        ok: true,
        response: true
      }
    } catch (err) {
      return handleError(err)
    }
  }

  // Campaign Templates
  async getCampaignTemplate(
    campaignTemplateId: string
  ): Promise<Response<CampaignTemplateResponse>> {
    try {
      return {
        ok: true,
        response: {
          campaignTemplate:
            await this.campaignTemplates.getCampaignTemplate(campaignTemplateId)
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async updateCampaignTemplate(
    campaignTemplateId: string,
    payload: CampaignTemplateCreateDto
  ): Promise<Response<undefined>> {
    try {
      await this.campaignTemplates.updateCampaignTemplate(
        campaignTemplateId,
        payload
      )

      return {
        ok: true,
        response: undefined
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async createCampaignTemplate(
    payload: CampaignTemplateCreateDto
  ): Promise<Response<CampaignTemplateResponse>> {
    try {
      return {
        ok: true,
        response: {
          campaignTemplate:
            await this.campaignTemplates.createCampaignTemplate(payload)
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  // Campaigns
  async getCampaign(
    _talent_profile_id: string,
    campaign_id: string
  ): Promise<Response<CampaignResponse>> {
    try {
      const campaign = await this.campaigns.getCampaign(campaign_id)

      const segments = await getSegmentServiceV1().getManyByIds(
        campaign.segmentIds
      )

      return {
        ok: true,
        response: {
          campaign: {
            ...campaign,
            segments: segments.map(x => ({
              segmentId: x.segmentId,
              segmentName: x.name
            })),
            status: CampaignStatus[campaign.status],
            schedule: {
              ...campaign.schedule,
              sendType: SendType[campaign.schedule.sendType]
            }
          }
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async getCampaignReport(
    _talent_profile_id: string,
    campaign_id: string
  ): Promise<Response<CampaignReportResponse>> {
    try {
      const campaignReport = await this.campaigns.getCampaignReport(campaign_id)

      const segments = await getSegmentServiceV1().getManyByIds(
        campaignReport.details.segmentIds
      )

      return {
        ok: true,
        response: {
          campaign: {
            ...campaignReport,
            status: CampaignStatus[campaignReport.status],
            details: {
              ...campaignReport.details,
              segments: segments.map(x => ({
                segmentId: x.segmentId,
                segmentName: x.name
              }))
            }
          }
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async getCampaigns(
    queryParams: PaginationQueryParameters,
    _talent_profile_id: string,
    searchTerm: string
  ): Promise<Response<CampaignsWithCursorResponse>> {
    try {
      const campaigns = await this.campaigns.getCampaigns({
        ...queryParams,
        direction: PaginationDirection[queryParams.direction],
        searchTerm
      })

      const segmentIds = uniq(campaigns.flatMap(x => x.segmentIds))
      const segments = await getSegmentServiceV1().getManyByIds(segmentIds)

      const segmentMap = new Map(
        segments.map(x => [
          x.segmentId,
          { segmentId: x.segmentId, segmentName: x.name }
        ])
      )

      if (segments.length !== segmentIds.length) {
        notification.error({
          message: 'Failed to fetch alls segments associated with campaigns'
        })
      }

      return {
        ok: true,
        response: {
          campaigns: campaigns.map(campaign => ({
            ...campaign,
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            segments: campaign.segmentIds.map(
              id =>
                segmentMap.get(id) ?? {
                  segmentId: '00000000-0000-0000-0000-000000000000',
                  segmentName: 'SEGMENT NOT FOUND'
                }
            ),
            status: CampaignStatus[campaign.status],
            schedule: {
              ...campaign.schedule,
              sendType: SendType[campaign.schedule.sendType]
            },
            cursor: campaign.cursor
          }))
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async getCampaignsCount(
    countParams: CountQueryParameters,
    _talent_profile_id: string,
    searchTerm: string
  ): Promise<Response<CountQueryResponse>> {
    try {
      return {
        ok: true,
        response: {
          count: await this.campaigns.getCampaignsCount({
            ...countParams,
            searchTerm
          })
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async createCampaign(
    _talent_profile_id: string,
    payload: CampaignUpsertDto
  ): Promise<Response<CampaignResponse>> {
    try {
      const campaign = await this.campaigns.createCampaign({
        ...payload,
        schedule: {
          ...payload.schedule,
          sendType: EmailCampaignSendType[payload.schedule.sendType]
        }
      })

      const segments = await getSegmentServiceV1().getManyByIds(
        campaign.segmentIds
      )

      return {
        ok: true,
        response: {
          campaign: {
            ...campaign,
            segments: segments.map(x => ({
              segmentId: x.segmentId,
              segmentName: x.name
            })),
            status: CampaignStatus[campaign.status],
            schedule: {
              ...campaign.schedule,
              sendType: SendType[campaign.schedule.sendType]
            }
          }
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async patchCampaign(
    _talent_profile_id: string,
    campaign_id: string,
    payload: DeepPartial<CampaignUpsertDto>
  ): Promise<Response<GetEmailDomainResponse>> {
    try {
      const { sendType, ...schedule } = payload.schedule ?? {}

      return {
        ok: true,
        response: await this.campaigns.updateCampaign(campaign_id, {
          ...payload,
          schedule: {
            ...schedule,
            ...(sendType ? { sendType: EmailCampaignSendType[sendType] } : {})
          },
          // NOTE: DeepPartial is messing is segmentIds type
          segmentIds: payload.segmentIds as string[] | undefined
        })
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async checkCampaignNameExists(
    talentProfileId: string,
    campaignId: string,
    name: string
  ): Promise<Response<CheckNameExistsResponse>> {
    try {
      return {
        ok: true,
        response: {
          success: true,
          nameExists: await this.campaigns.checkNameExists({
            name,
            talentProfileId,
            campaignId
          })
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async stopCampaign(
    _talentProfileId: string,
    campaignId: string
  ): Promise<Response<{ updatedCampaign: EmailCampaignSchema }>> {
    try {
      return {
        ok: true,
        response: {
          updatedCampaign: await this.campaigns.stopCampaign(campaignId)
        }
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async restartCampaign(
    _talentProfileId: string,
    campaignId: string
  ): Promise<Response<undefined>> {
    try {
      await this.campaigns.restartCampaign(campaignId)

      return {
        ok: true,
        response: undefined
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async deleteCampaign(campaignId: string): Promise<Response<boolean>> {
    try {
      await this.campaigns.deleteCampaign(campaignId)

      return {
        ok: true,
        response: true
      }
    } catch (err) {
      return handleError(err)
    }
  }

  // Custom domain
  async getEmailDomain(): Promise<Response<GetEmailDomainResponse>> {
    try {
      return {
        ok: true,
        response: await this.mailDomain.getMailDomain()
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async deleteEmailDomain(): Promise<Response<undefined>> {
    try {
      await this.mailDomain.deleteMailDomain()

      return {
        ok: true,
        response: undefined
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async postEmailDomain(
    data: PostEmailDomainRequest
  ): Promise<Response<GetEmailDomainResponse>> {
    try {
      return {
        ok: true,
        response: await this.mailDomain.authenticateMailDomain(data)
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async verifyEmailDomain(): Promise<Response<GetEmailDomainResponse>> {
    try {
      return {
        ok: true,
        response: await this.mailDomain.verifyMailDomain()
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async patchEmailDomain(
    data: PatchEmailDomainRequest
  ): Promise<Response<GetEmailDomainResponse>> {
    try {
      return {
        ok: true,
        response: await this.mailDomain.updateMailDomain(data)
      }
    } catch (err) {
      return handleError(err)
    }
  }

  async sendTestEmail(
    data: PostSendTestEmailRequest
  ): Promise<Response<undefined>> {
    try {
      await this.campaigns.sendTestEmail(data)

      return {
        ok: true,
        response: undefined
      }
    } catch (err) {
      return handleError(err)
    }
  }
}

async function handleError(err: any): Promise<GeneralApiProblem> {
  if (err instanceof ResponseError) {
    const status = err.status

    // Only handle 4xx errors
    if (status >= 400 && status < 500) {
      const message = await getMessageFromResponse(err.response)

      switch (status) {
        case 401:
          return {
            kind: 'unauthorized',
            message: message ?? 'Unauthorized',
            messageTx: 'errors.auth_invalid_token',
            status
          }
        case 403:
          return {
            kind: 'forbidden',
            message: message ?? 'Unauthorized',
            status
          }
        case 404:
          return {
            kind: 'not-found',
            message: message ?? 'Not Found',
            status
          }
        case 405:
          return {
            kind: 'not-allow',
            message: message ?? 'Session expired, please login again.',
            status
          }
        default: {
          return {
            kind: 'rejected',
            message: message ?? messagingServiceDefaultErrorMessage,
            status
          }
        }
      }
    }
  }

  return {
    kind: 'server',
    status: 500,
    message: messagingServiceDefaultErrorMessage
  }
}

async function getMessageFromResponse(
  response: globalThis.Response
): Promise<string | null> {
  try {
    const body = await response.json()

    let message = body.message

    if (Array.isArray(message)) {
      message = message[0].message
    }

    return message
  } catch {
    return null
  }
}
