import {
  Arg,
  Args,
  FieldResolver,
  ID,
  Int,
  Mutation,
  Query,
  Resolver,
  Root,
} from "@aet/gql-tools/macro"
import browser from "webextension-polyfill"
import { customAlphabet } from "nanoid/non-secure"
import { File, FileFilter, FindFilterArgs, PartialFileInput } from "~/shared/models/index"
import { FileSortBy, FileState, fileMatches } from "~/shared/models/utils"
import { statistics } from "../../idb"
import { deleteDoc, exists, getDocs, localDb } from "../../couchdb"
import { GLOBAL_STYLE_ID, isGlobalStyle } from "~/shared"
import { broadcast } from "../utils"
import { guessFileLanguage } from "../utils/guessLanguage"
import { PreferencesCache } from "./PreferencesResolver"
import { isFileLoggingDisabled } from "./StatisticsResolver"

@Resolver(() => File)
export class FileResolver {
  private readonly nanoId = customAlphabet("1234567890abcdef", 24)

  @Query()
  async file(@Arg() id: ID): Promise<File> {
    return await localDb.get(id)
  }

  private static fileMatches(file: File, filter: FileFilter) {
    // To add a filter: (filterExists && !filterMatchesFile) ||
    return !(
      (filter.enabled != null && file.enabled !== filter.enabled) ||
      (filter.tags && !filter.tags.some(tag => file.tags.includes(tag))) ||
      (filter.url != null && !fileMatches(file, filter.url)) ||
      (filter.id?.length && !filter.id.includes(file.id)) ||
      (filter.state != null && file.state !== filter.state) ||
      (filter.isGlobal != null && isGlobalStyle(file) !== filter.isGlobal)
    )
  }

  @Query()
  async files(
    @Args() { filter, take, sortBy, logQuery }: FindFilterArgs,
  ): Promise<File[]> {
    let files = await getDocs()

    if (filter) {
      files = files.filter(file => FileResolver.fileMatches(file, filter))
    }

    if (take) {
      files = files.slice(0, take)
    }

    switch (sortBy) {
      case FileSortBy.Name:
        files = files.sort((a, b) => a.name.localeCompare(b.name))
        break
      case FileSortBy.UpdatedAt:
        files = files.sort((a, b) => b.updatedAt - a.updatedAt)
        break

      default:
      // noop;
    }

    if (PreferencesCache.cache.get().recordUsageData && logQuery) {
      files
        .filter(file => !isFileLoggingDisabled(file.id))
        .forEach(file => {
          void statistics.set(file.id, { lastRequested: new Date() })
        })
    }

    return files
  }

  async #newFile(input: PartialFileInput): Promise<File> {
    const id = this.nanoId()
    const created = new Date()
    const file = new File({
      id,
      name: input.name! || browser.i18n.getMessage("untitled"),
      mimeType: "text/x-scss",
      enabled: true,
      tags: [],
      createdAt: created,
      updatedAt: created,
      rules: [],
      hidden: false,
      state: FileState.None,
      sections: [
        {
          id: this.nanoId(),
          name: "",
          code: "",
          enabled: true,
        },
      ],
      ...input,
    })
    await localDb.put({
      _id: id,
      ...file,
    })
    return file
  }

  @Mutation()
  async newFile(@Arg() input: PartialFileInput): Promise<File> {
    const file = await this.#newFile(input)
    void broadcast()
    return file
  }

  @Mutation()
  async createGlobalStyle(): Promise<File> {
    if (await exists(GLOBAL_STYLE_ID)) {
      throw new Error("Global style already exists")
    }

    const created = new Date()
    const file = new File({
      id: GLOBAL_STYLE_ID,
      name: "__global__",
      enabled: true,
      mimeType: "text/x-scss",
      tags: ["global"],
      createdAt: created,
      updatedAt: created,
      rules: [],
      hidden: false,
      state: FileState.None,
      sections: [
        {
          id: this.nanoId(),
          name: "",
          code: "",
          enabled: true,
        },
      ],
    })
    await localDb.put({
      _id: GLOBAL_STYLE_ID,
      ...file,
    })
    void broadcast()
    return file
  }

  @Mutation()
  async newFiles(@Arg() inputs: PartialFileInput[]): Promise<File[]> {
    const files = await Promise.all(inputs.map(input => this.newFile(input)))
    void broadcast()
    return files
  }

  @Mutation()
  async deleteFile(@Arg() id: ID) {
    await deleteDoc(id)
    void broadcast()
    return true
  }

  @Mutation()
  async deleteFiles(@Arg() ids: ID[]) {
    await Promise.all(ids.map(id => deleteDoc(id)))
    void broadcast()
    return true
  }

  @Mutation()
  async setFile(@Arg() id: ID, @Arg() input: PartialFileInput): Promise<File> {
    const file = await setFile(id, input)
    void broadcast()
    return file
  }

  @Mutation()
  async setFiles(@Arg() ids: ID[], @Arg() input: PartialFileInput): Promise<File[]> {
    const files = await Promise.all(ids.map(fileId => setFile(fileId, input)))
    void broadcast()
    return files
  }

  @Mutation()
  async deleteAllFiles() {
    await localDb.destroy()
    void broadcast()
    return true
  }

  @FieldResolver()
  nameLanguage(@Root() file: File): string | undefined {
    return guessFileLanguage(file)
  }

  @FieldResolver()
  lastRequestedAt(@Root() file: File): Date | undefined {
    return statistics.get(file.id)?.lastRequested
  }

  @FieldResolver()
  size(@Root() file: File): Int {
    return file.sections.reduce((acc, section) => acc + section.code.length, 0)
  }
}

export async function setFile(id: ID, input: PartialFileInput): Promise<File> {
  const existing = await localDb.get(id)!
  const now = new Date()

  const file = new File({
    ...existing,
    ...(input as Partial<File>),
    updatedAt: now,
  })

  await localDb.put(file)

  return file
}
