import { diff, ChangeType } from '@graphql-inspector/core'
import { loadSchema } from '@graphql-tools/load'
import fs from 'fs'
import { renderContent } from '#src/content-render/index.js'

/**
 * Tag `changelogEntry` with `date: YYYY-mm-dd`, then prepend it to the JSON
 * structure written to `targetPath`. (`changelogEntry` and that file are modified in place.)
 * @param {object} changelogEntry
 * @param {string} targetPath
 * @return {void}
 */
export function prependDatedEntry(changelogEntry, targetPath) {
  // Build a `yyyy-mm-dd`-formatted date string
  // and tag the changelog entry with it
  const todayString = new Date().toISOString().slice(0, 10)
  changelogEntry.date = todayString

  const previousChangelogString = fs.readFileSync(targetPath)
  const previousChangelog = JSON.parse(previousChangelogString)
  // add a new entry to the changelog data
  previousChangelog.unshift(changelogEntry)
  // rewrite the updated changelog
  fs.writeFileSync(targetPath, JSON.stringify(previousChangelog, null, 2))
}

/**
 * Compare `oldSchemaString` to `newSchemaString`, and if there are any
 * changes that warrant a changelog entry, return a changelog entry.
 * Based on the parsed `previews`, identify changes that are under a preview.
 * Otherwise, return null.
 * @param {string} [oldSchemaString]
 * @param {string} [newSchemaString]
 * @param {Array<object>} [previews]
 * @param {Array<object>} [oldUpcomingChanges]
 * @param {Array<object>} [newUpcomingChanges]
 * @return {object?}
 */
export async function createChangelogEntry(
  oldSchemaString,
  newSchemaString,
  previews,
  oldUpcomingChanges,
  newUpcomingChanges,
) {
  // Create schema objects out of the strings
  const oldSchema = await loadSchema(oldSchemaString, {})
  const newSchema = await loadSchema(newSchemaString, {})

  // Generate changes between the two schemas
  const changes = await diff(oldSchema, newSchema)
  const changesToReport = []
  changes.forEach((change) => {
    if (CHANGES_TO_REPORT.includes(change.type)) {
      changesToReport.push(change)
    } else if (CHANGES_TO_IGNORE.includes(change.type)) {
      // Do nothing
    } else {
      console.error('Change object causing error:')
      console.error(change)
      throw new Error(
        'This change type should be added to CHANGES_TO_REPORT or CHANGES_TO_IGNORE: ' +
          change.type,
      )
    }
  })

  const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges(
    changesToReport,
    previews,
  )

  const addedUpcomingChanges = newUpcomingChanges.filter(function (change) {
    // Manually check each of `newUpcomingChanges` for an equivalent entry
    // in `oldUpcomingChanges`.
    return !oldUpcomingChanges.find(function (oldChange) {
      return (
        oldChange.location === change.location &&
        oldChange.date === change.date &&
        oldChange.description === change.description
      )
    })
  })

  // If there were any changes, create a changelog entry
  if (
    schemaChangesToReport.length > 0 ||
    previewChangesToReport.length > 0 ||
    addedUpcomingChanges.length > 0
  ) {
    const changelogEntry = {
      schemaChanges: [],
      previewChanges: [],
      upcomingChanges: [],
    }

    const cleanedSchemaChanges = cleanMessagesFromChanges(schemaChangesToReport)
    const renderedScheamChanges = await Promise.all(
      cleanedSchemaChanges.map(async (change) => {
        return await renderContent(change)
      }),
    )
    const schemaChange = {
      title: 'The GraphQL schema includes these changes:',
      // Replace single quotes which wrap field/argument/type names with backticks
      changes: renderedScheamChanges,
    }
    changelogEntry.schemaChanges.push(schemaChange)

    for (const previewTitle in previewChangesToReport) {
      const previewChanges = previewChangesToReport[previewTitle]
      const cleanedPreviewChanges = cleanMessagesFromChanges(previewChanges.changes)
      const renderedPreviewChanges = await Promise.all(
        cleanedPreviewChanges.map(async (change) => {
          return renderContent(change)
        }),
      )
      const cleanTitle = cleanPreviewTitle(previewTitle)
      const entryTitle =
        'The [' +
        cleanTitle +
        '](/graphql/overview/schema-previews#' +
        previewAnchor(cleanTitle) +
        ') includes these changes:'
      changelogEntry.previewChanges.push({
        title: entryTitle,
        changes: renderedPreviewChanges,
      })
    }

    if (addedUpcomingChanges.length > 0) {
      const cleanedUpcomingChanges = addedUpcomingChanges.map((change) => {
        const location = change.location
        const description = change.description
        const date = change.date.split('T')[0]
        return 'On member `' + location + '`:' + description + ' **Effective ' + date + '**.'
      })
      const renderedUpcomingChanges = await Promise.all(
        cleanedUpcomingChanges.map(async (change) => {
          return await renderContent(change)
        }),
      )
      changelogEntry.upcomingChanges.push({
        title: 'The following changes will be made to the schema:',
        changes: renderedUpcomingChanges,
      })
    }

    return changelogEntry
  } else {
    return null
  }
}

/**
 * Prepare the preview title from github/github source for the docs.
 * @param {string} title
 * @return {string}
 */
export function cleanPreviewTitle(title) {
  if (title === 'UpdateRefsPreview') {
    title = 'Update refs preview'
  } else if (title === 'MergeInfoPreview') {
    title = 'Merge info preview'
  } else if (!title.endsWith('preview')) {
    title = title + ' preview'
  }
  return title
}

/**
 * Turn the given title into an HTML-ready anchor.
 * (ported from graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L281)
 * @param {string} [previewTitle]
 * @return {string}
 */
export function previewAnchor(previewTitle) {
  return previewTitle
    .toLowerCase()
    .replace(/ /g, '-')
    .replace(/[^\w-]/g, '')
}

/**
 * Turn changes from graphql-inspector into messages for the HTML changelog.
 * @param {Array<object>} changes
 * @return {Array<string>}
 */
export function cleanMessagesFromChanges(changes) {
  return changes.map(function (change) {
    // replace single quotes around graphql names with backticks,
    // to match previous behavior from graphql-schema-comparator
    return change.message.replace(/'([a-zA-Z. :!]+)'/g, '`$1`')
  })
}

/**
 * Split `changesToReport` into two parts,
 * one for changes in the main schema,
 * and another for changes that are under preview.
 * (Ported from /graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L230)
 * @param {Array<object>} changesToReport
 * @param {object} previews
 * @return {object}
 */
export function segmentPreviewChanges(changesToReport, previews) {
  // Build a map of `{ path => previewTitle` }
  // for easier lookup of change to preview
  const pathToPreview = {}
  previews.forEach(function (preview) {
    preview.toggled_on.forEach(function (path) {
      pathToPreview[path] = preview.title
    })
  })
  const schemaChanges = []
  const changesByPreview = {}

  changesToReport.forEach(function (change) {
    // For each change, see if its path _or_ one of its ancestors
    // is covered by a preview. If it is, mark this change as belonging to a preview
    const pathParts = change.path.split('.')
    let testPath = null
    let previewTitle = null
    let previewChanges = null
    while (pathParts.length > 0 && !previewTitle) {
      testPath = pathParts.join('.')
      previewTitle = pathToPreview[testPath]
      // If that path didn't find a match, then we'll
      // check the next ancestor.
      pathParts.pop()
    }
    if (previewTitle) {
      previewChanges =
        changesByPreview[previewTitle] ||
        (changesByPreview[previewTitle] = {
          title: previewTitle,
          changes: [],
        })
      previewChanges.changes.push(change)
    } else {
      schemaChanges.push(change)
    }
  })
  return { schemaChangesToReport: schemaChanges, previewChangesToReport: changesByPreview }
}

// We only want to report changes to schema structure.
// Deprecations are covered by "upcoming changes."
// By listing the changes explicitly here, we can make sure that,
// if the library changes, we don't miss publishing anything that we mean to.
// This was originally ported from graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L35-L103
const CHANGES_TO_REPORT = [
  ChangeType.FieldArgumentDefaultChanged,
  ChangeType.FieldArgumentTypeChanged,
  ChangeType.EnumValueRemoved,
  ChangeType.EnumValueAdded,
  ChangeType.FieldRemoved,
  ChangeType.FieldAdded,
  ChangeType.FieldTypeChanged,
  ChangeType.FieldArgumentAdded,
  ChangeType.FieldArgumentRemoved,
  ChangeType.ObjectTypeInterfaceAdded,
  ChangeType.ObjectTypeInterfaceRemoved,
  ChangeType.InputFieldRemoved,
  ChangeType.InputFieldAdded,
  ChangeType.InputFieldDefaultValueChanged,
  ChangeType.InputFieldTypeChanged,
  ChangeType.TypeRemoved,
  ChangeType.TypeAdded,
  ChangeType.TypeKindChanged,
  ChangeType.UnionMemberRemoved,
  ChangeType.UnionMemberAdded,
  ChangeType.SchemaQueryTypeChanged,
  ChangeType.SchemaMutationTypeChanged,
  ChangeType.SchemaSubscriptionTypeChanged,
]

const CHANGES_TO_IGNORE = [
  ChangeType.FieldArgumentDescriptionChanged,
  ChangeType.DirectiveRemoved,
  ChangeType.DirectiveAdded,
  ChangeType.DirectiveDescriptionChanged,
  ChangeType.DirectiveLocationAdded,
  ChangeType.DirectiveLocationRemoved,
  ChangeType.DirectiveArgumentAdded,
  ChangeType.DirectiveArgumentRemoved,
  ChangeType.DirectiveArgumentDescriptionChanged,
  ChangeType.DirectiveArgumentDefaultValueChanged,
  ChangeType.DirectiveArgumentTypeChanged,
  ChangeType.EnumValueDescriptionChanged,
  ChangeType.EnumValueDeprecationReasonChanged,
  ChangeType.EnumValueDeprecationReasonAdded,
  ChangeType.EnumValueDeprecationReasonRemoved,
  ChangeType.FieldDescriptionChanged,
  ChangeType.FieldDescriptionAdded,
  ChangeType.FieldDescriptionRemoved,
  ChangeType.FieldDeprecationAdded,
  ChangeType.FieldDeprecationRemoved,
  ChangeType.FieldDeprecationReasonChanged,
  ChangeType.FieldDeprecationReasonAdded,
  ChangeType.FieldDeprecationReasonRemoved,
  ChangeType.InputFieldDescriptionAdded,
  ChangeType.InputFieldDescriptionRemoved,
  ChangeType.InputFieldDescriptionChanged,
  ChangeType.TypeDescriptionChanged,
  ChangeType.TypeDescriptionRemoved,
  ChangeType.TypeDescriptionAdded,
  ChangeType.DirectiveUsageFieldDefinitionAdded,
  ChangeType.DirectiveUsageArgumentDefinitionAdded,
  ChangeType.DirectiveUsageEnumValueAdded,
]

export default { createChangelogEntry, cleanPreviewTitle, previewAnchor, prependDatedEntry }