import fsImport from 'fs'
import util from 'util'
import childProcess from 'child_process'
import repos from './src/repos.ts'
import branches from './src/branches.ts'
import flatFiles from './src/flatFiles.ts'
import flatPatches from './src/flatPatches.ts'
import paginatedPatches, {type PatchPage} from './src/paginatedPatches.ts'
import {getLocation} from './src/helpers.ts'
import * as operations from './src/vcses/git/operations.ts'
import {ReposConfiguration} from './src/configTypes.ts'
import {Ajv} from 'ajv'
import ConfigSchema from './schemas/ReposConfiguration.json' with { type: 'json' }

const ajv = new Ajv()
const exec = util.promisify(childProcess.exec)

export default async function repoViewer(eleventyConfig: any, reposConfiguration: ReposConfiguration) {
  const validator = ajv.compile(ConfigSchema)
  const valid = validator(reposConfiguration)
  if (!valid) {
    throw new Error(validator.errors.map(error => `config object at ${error.instancePath.replaceAll("/", ".")}: ${error.message}`).join("\n"))
  }

  const reposData = await repos(reposConfiguration)
  // TODO: make a better way of making this default to "" so that it doesn't have to
  // be done again in src/repos.ts.
  const reposPath = reposConfiguration.path || ""

  eleventyConfig.addGlobalData("repos", reposData)
  eleventyConfig.addGlobalData("reposConfig", reposConfiguration)
  eleventyConfig.addGlobalData("reposPath", reposPath)

  const branchesData = branches(reposData)
  eleventyConfig.addGlobalData("branches", branchesData)

  eleventyConfig.addFilter("getFileName", (filePath: string) => {
    const pathParts = filePath.split("/")
    return pathParts[pathParts.length - 1]
  })

  const vendor = `${import.meta.dirname}/vendor`
  const frontend = `${import.meta.dirname}/frontend`

  eleventyConfig.addPassthroughCopy({
    [vendor]: `${reposPath}/vendor`,
    [frontend]: `${reposPath}/frontend`,
  })

  eleventyConfig.on(
    "eleventy.after",
    async ({ directories }) => {
      const cwd = process.cwd()
      // Check to see if there is already a repo in all of the locations
      // that should have one.
      for (let repoName in reposConfiguration.repos) {
        const repoConfig = reposConfiguration.repos[repoName]
        const repoPath = eleventyConfig.dir.output + reposPath + "/" + eleventyConfig.getFilter("slugify")(repoName)
        const gitRepoName = eleventyConfig.getFilter("slugify")(repoName) + ".git"
        // If it is there, do git pull
        if (fsImport.existsSync(repoPath + ".git")) {
          // git repos are just in the repos folder, not in their subdir
          // create string of commands saying 'git fetch origin branch:branch' for each branch
          const fetchCommands = repoConfig.branchesToPull.map(branch => `git fetch origin ${branch}:${branch}`).join('; ')
          await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} && ${fetchCommands}; git update-server-info)`)
        } else {
          // If it is not there, do git clone
          const originalLocation = cwd + "/" + repoConfig.location
          await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/"} && git clone ${originalLocation} ${gitRepoName} --bare)`)
          await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} && git update-server-info)`)
        }
        if (typeof repoConfig.artifactSteps !== 'undefined') {
          // make a temp directory for things to run in
          const tempDirName = `temp_${Math.floor(Math.random() * 10000).toString()}`
          const tempDir = `${directories.output}${reposPath}${tempDirName}`
          const tempDirRepoPath = `${tempDir}/${eleventyConfig.getFilter("slugify")(repoName)}`
          await exec(`mkdir ${tempDir}`)
          await exec(`git clone -s ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}.git ${tempDirRepoPath}`)
          for (let branch of repoConfig.branchesToPull) {
            await exec(`(cd ${tempDirRepoPath} && git checkout ${branch})`)
            for (let artifactStep of repoConfig.artifactSteps) {
              // Run the command for each step in each branch
              await exec(`(cd ${tempDirRepoPath} && ${artifactStep.command})`)
              // Copy the specified folders from the "from" to the "to" dir
              await exec(`cp -r --remove-destination ${tempDirRepoPath}/${artifactStep.copyFrom} ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branch)}/${artifactStep.copyTo}`)
            }
          }
          // delete the temp dirs
          await exec(`rm -r ${tempDir}`)
        }
      }
    }
  );

  eleventyConfig.addFilter("getDirectoryContents", (repo: string, branch: string, dirPath: string) => {
    return reposData.find(current => current.name === repo).branches.find(current => current.name === branch).fileList.filter(file => file.startsWith(dirPath) && file !== dirPath)
  })

  eleventyConfig.addFilter("getRelativePath", (currentDir: string, fullFilePath: string) => {
    return fullFilePath.replace(`${currentDir}/`, "")
  })

  eleventyConfig.addFilter("lineNumbers", (code: string) => {
    const numLines = code.split('\n').length
    const lineNumbers = []
    for (let i = 1; i <= numLines; i++) {
      lineNumbers.push(i)
    }

    return lineNumbers
  })

  eleventyConfig.addFilter("highlightCode", (code: string, language: string) => {
    const highlighter = eleventyConfig?.javascript?.functions?.highlight
    if (highlighter) {
      return highlighter(language, code)
    }
    else {
      return code
    }
  })

  eleventyConfig.addAsyncFilter("renderContentIfAvailable", async (contentString: string, contentType: string) => {
    const renderer = eleventyConfig?.javascript?.functions?.renderContent
    if (renderer) {
      return await renderer.bind({})(contentString, contentType)
    }
    else {
      return `<pre>${contentString}</pre>`
    }
  })

  eleventyConfig.addFilter("languageExtension", (filename: string, repoName: string) => {
    let filenameParts = filename.split(".")
    let extension = filenameParts[filenameParts.length - 1]
    const extensionsConfig = reposConfiguration.repos[repoName].languageExtensions
    return extensionsConfig && extensionsConfig[extension] ? extensionsConfig[extension] : extension
  })

  eleventyConfig.addFilter("topLevelFilesOnly", (files: Array<string>, currentLevel: string) => {
    const onlyUnique = (value, index, array) => {
      return array.findIndex(test => test.name === value.name) === index;
    }

    const currentLevelDirLength = currentLevel.split('/').length

    const topLevels: Array<string> = []
    files.forEach((file) => {
      if (file.startsWith(currentLevel)) {
        const parts = file.split("/").filter(part => part !== ".")
        topLevels.push(parts.slice(0, currentLevelDirLength).join('/'))
      }
    })

    const withNameAndDirAttrs = topLevels.map((file) => {
      // is a directory if the entire filename, plus a slash, is contained inside of any
      // other file
      const isDirectory: boolean = files.some((testFile) => {
        return testFile.startsWith(file + '/') && (testFile !== file)
      })

      return {name: file.replace(currentLevel, ''), fullPath: file, isDirectory}
    })

    const sortedByDirectory = withNameAndDirAttrs.filter(onlyUnique).toSorted((a, b) => {
      if (a.isDirectory && b.isDirectory) {
        return 0
      }
      if (a.isDirectory && !b.isDirectory) {
        return -1
      }
      return 1
    })

    return sortedByDirectory
  })

  eleventyConfig.addAsyncFilter("getFileLastTouchInfo", async (repo: string, branch: string, filename: string) => {
    const ignoreExtensions = ['.png', '.jpg']
    if (ignoreExtensions.some(extension => filename.endsWith(extension))) {
      return ""
    }

    const location = getLocation(reposConfiguration, repo)
    return operations.getFileLastTouchInfo(branch, filename, location)
  })

  eleventyConfig.addFilter("isDirectory", (filename: string, repoName: string, branchName: string) => {
    const repo = reposData.find(repo => repo.name === repoName)
    const files = repo.branches.find(branch => branch.name === branchName).fileList
    const isDirectory = files.some((testFile) => {
        return testFile.startsWith(filename + '/') && (testFile !== filename)
      })
    return isDirectory
  })

  eleventyConfig.addAsyncFilter("getFileContents", async (repo: string, branch: string, filename: string) => {
    const location = getLocation(reposConfiguration, repo)
    const command = `git show ${branch}:${filename}`
    const res = await exec(`(cd ${location} && ${command})`)
    return res.stdout
  })

  eleventyConfig.addFilter("pagesJustForBranch", (pages: Array<PatchPage>, repoName: string, branchName: string) => {
    return pages.filter(page => page.repoName === repoName && page.branchName === branchName)
  })

  eleventyConfig.addFilter("date", (dateString: string) => {
    return new Date(dateString).toDateString()
  })

  eleventyConfig.addFilter("toDateObj", (dateString: string) => {
    return new Date(dateString)
  })

  eleventyConfig.addAsyncFilter("getReadMe", async (repoName: string, branchName: string) => {
    const location = getLocation(reposConfiguration, repoName)
    const command = `git show ${branchName}:README.md`
    try {
      const res = await exec(`(cd ${location} && ${command})`)
      return res.stdout
    } catch {
      return ""
    }
  })

  eleventyConfig.addFilter("jsonStringify", data => JSON.stringify(data))

  const topLayoutPartial = fsImport.readFileSync(`${import.meta.dirname}/partial_templates/main_top.njk`).toString()
  const bottomLayoutPartial = fsImport.readFileSync(`${import.meta.dirname}/partial_templates/main_bottom.njk`).toString()

  // INDEX.NJK
  const indexTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/index.njk`).toString()
  eleventyConfig.addTemplate(
    'repos/index.njk',
    topLayoutPartial + indexTemplate + bottomLayoutPartial,
    {
      permalink: `${reposPath}/index.html`,
    }
  )

  // BRANCHES.NJK
  const branchesTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/branches.njk`).toString()
  eleventyConfig.addTemplate(
    'repos/branches.njk',
    topLayoutPartial + branchesTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "branches",
        size: 1,
        alias: "branchInfo",
      },
      permalink: (data) => {
        const repoName = data.branchInfo.repoName
        const branchName = data.branchInfo.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/list/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.branchInfo.repoName,
          branchName: (data) => data.branchInfo.branchName,
          path: "list"
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.branchInfo.repoName
        }),
        currentBranch: (data) => reposData.find(repo => {
          return repo.name === data.branchInfo.repoName
        }).branches.find(branch => {
          return branch.name === data.branchInfo.branchName
        }),
      },
      navTab: "branches",
    }
  )

  // FILE.NJK
  const fileTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/file.njk`).toString()
  const flatFilesData = flatFiles(reposData)
  eleventyConfig.addTemplate(
    'repos/file.njk',
    topLayoutPartial + fileTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "flatFiles",
        size: 1,
        alias: "fileInfo",
      },
      flatFiles: flatFilesData,
      permalink: (data) => {
        const repoName = data.fileInfo.repoName
        const branchName = data.fileInfo.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/files/${eleventyConfig.getFilter("slugify")(data.fileInfo.file)}.html`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.fileInfo.repoName,
          branchName: (data) => data.fileInfo.branchName,
          path: "files",
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.fileInfo.repoName
        }),
        currentBranch: (data) => reposData.find(repo => {
          return repo.name === data.fileInfo.repoName
        }).branches.find(branch => {
          return branch.name === data.fileInfo.branchName
        }),
      },
      navTab: "files",
    }
  )

  // RAW.NJK
  const rawTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/raw.njk`).toString()
  eleventyConfig.addTemplate(
    'repos/raw.njk',
    rawTemplate,
    {
      pagination: {
        data: "flatFiles",
        size: 1,
        alias: "fileInfo",
      },
      eleventyAllowMissingExtension: true,
      flatFiles: flatFilesData,
      permalink: (data) => {
        const repoName = data.fileInfo.repoName
        const branchName = data.fileInfo.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/raw/${eleventyConfig.getFilter("slugify")(data.fileInfo.file)}`
      },
    }
  )

  // FILES.NJK
  const filesTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/files.njk`).toString()
  eleventyConfig.addTemplate(
    'repos/files.njk',
    topLayoutPartial + filesTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "branches",
        size: 1,
        alias: "branchInfo",
      },
      permalink: (data) => {
        const repoName = data.branchInfo.repoName
        const branchName = data.branchInfo.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/files/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.branchInfo.repoName,
          branchName: (data) => data.branchInfo.branchName,
          path: "files",
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.branchInfo.repoName
        }),
        currentBranch: (data) => reposData.find(repo => {
          return repo.name === data.branchInfo.repoName
        }).branches.find(branch => {
          return branch.name === data.branchInfo.branchName
        })
      },
      navTab: "files",
    }
  )

  // REPO.NJK
  const repoTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/repo.njk`).toString()
  eleventyConfig.addTemplate(
    'repos/repo.njk',
    topLayoutPartial + repoTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "branches",
        size: 1,
        alias: "branch",
      },
      permalink: (data) => {
        const repoName = data.branch.repoName
        const branchName = data.branch.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.branch.repoName,
          branchName: (data) => data.branch.branchName,
          path: "",
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.branch.repoName
        }),
        currentBranch: (data) => reposData.find(repo => {
          return repo.name === data.branch.repoName
        }).branches.find(branch => {
          return branch.name === data.branch.branchName
        }),
      },
      navTab: "landing",
    }
  )

  // PATCHES.NJK
  const patchesTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/patches.njk`).toString()
  const paginatedPatchesData = await paginatedPatches(reposData)
  eleventyConfig.addTemplate(
    `repos/patches.njk`,
    topLayoutPartial + patchesTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "paginatedPatches",
        size: 1,
        alias: "patchPage",
      },
      paginatedPatches: paginatedPatchesData,
      permalink: (data) => {
        const repoName = data.patchPage.repoName
        const branchName = data.patchPage.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/patches/page${data.patchPage.pageNumber}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.patchPage.repoName,
          branchName: (data) => data.patchPage.branchName,
          path: "patches/page1",
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.patchPage.repoName
        }),
        currentBranch: (data) => reposData.find(repo => {
          return repo.name === data.patchPage.repoName
        }).branches.find(branch => {
          return branch.name === data.patchPage.branchName
        }),
      },
      navTab: "patches",
    }
  )

  // PATCH.NJK
  const patchTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/patch.njk`).toString()
  const flatPatchesData = await flatPatches(reposData)
  eleventyConfig.addTemplate(
    `repos/patch.njk`,
    topLayoutPartial + patchTemplate + bottomLayoutPartial,
    {
      pagination: {
        data: "flatPatches",
        size: 1,
        alias: "patchInfo",
      },
      flatPatches: flatPatchesData,
      permalink: (data) => {
        const repoName = data.patchInfo.repoName
        const branchName = data.patchInfo.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/patches/${data.patchInfo.commit.hash}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.patchInfo.repoName,
          branchName: (data) => data.patchInfo.branchName,
          path: "patches/page1",
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.patchInfo.repoName
        }),
        currentBranch: (data) => reposData.find(repo => {
          return repo.name === data.patchInfo.repoName
        }).branches.find(branch => {
          return branch.name === data.patchInfo.branchName
        }),
      },
      width: "full",
      navTab: "patches",
    }
  )

  // FEED.NJK
  // htmlBaseUrl is a function defined by the 11ty RSS plugin.
  // Skip this virtual template if the 11ty RSS plugin is not being used.
  let rssAvailable = false
  // if (eleventyConfig?.javascript?.functions?.htmlBaseUrl) {
  //   rssAvailable = true
  //   const feedTemplate = fsImport.readFileSync(`${import.meta.dirname}/templates/feed.njk`).toString()
  //   eleventyConfig.addTemplate(
  //     `repos/feed.njk`,
  //     feedTemplate,
  //     {
  //       pagination: {
  //         data: "branches",
  //         size: 1,
  //         alias: "branch",
  //       },
  //       permalink: (data) => {
  //         const repoName = data.branch.repoName
  //         const branchName = data.branch.branchName
  //         return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/patches.xml`
  //       },
  //       eleventyComputed: {
  //         currentRepo: (data) => reposData.find(repo => {
  //           return repo.name === data.branch.repoName
  //         }),
  //         currentBranch: (data) => reposData.find(repo => {
  //           return repo.name === data.branch.repoName
  //         }).branches.find(branch => {
  //           return branch.name === data.branch.branchName
  //         }),
  //       },
  //       eleventyExcludeFromCollections: true,
  //     }
  //   )
  // }

  // This is used to show/hide the RSS feed link on the landing page.
  eleventyConfig.addGlobalData('rssAvailable', rssAvailable)
}
