import fsImport from 'fs'
import util from 'util'
import childProcess from 'child_process'
import {repos, getBranchesAndTags} from './src/repos.ts'
import getFlatRels from './src/flatRels.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 {ReposConfiguration} from './src/configTypes.ts'
import { type SortedFileList } from './src/dataTypes.ts'
import {Ajv} from 'ajv'
import ConfigSchema from './schemas/ReposConfiguration.json' with { type: 'json' }
import commonPage from './js_templates/common/commonPage.ts'
import repoJsTemplate from './js_templates/repo.ts'
import filesJsTemplate from './js_templates/files.ts'
import fileJsTemplate from './js_templates/file.ts'
import commitJsTemplate from './js_templates/commit.ts'
import commitsJsTemplate from './js_templates/commits.ts'
import indexJsTemplate from './js_templates/index.ts'
import branchesJsTemplate from './js_templates/branches.ts'
import tagsJsTemplate from './js_templates/tags.ts'
import rawJsTemplate from './js_templates/raw.ts'
import feedJsTemplate from './js_templates/feed.ts'

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

// TODO document how people need to do this and why. And maybe file 11ty bug?
export function beforeHook(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"))
  }

  return async ({directories}) => {
    const cwd = process.cwd()
    const reposPath = reposConfiguration.path || ""
    // 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 = directories.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 location = directories.output + reposPath + "/" + gitRepoName
        const {branches, tags} = await getBranchesAndTags(repoConfig, repoName)
        const branchNames = branches.map(branch => branch.name)
        const tagNames = branches.map(tag => tag.name)
        const fetchCommands = branchNames.concat(tagNames).map(ref => `git -C ${location} fetch origin ${ref}:${ref}`).join('; ')
        await exec(`${fetchCommands} && git -C ${location} update-server-info`)
      } else {
        // If it is not there, do git clone
        // todo: does this work if the latest branch is not checked
        // out locally?
        let originalLocation = cwd + "/" + repoConfig.location
        if (repoConfig.location.startsWith("https://") || repoConfig.location.startsWith("ssh://")) {
          originalLocation = repoConfig.location
        }
        await exec(`git clone ${originalLocation} ${directories.output + reposPath + "/" + gitRepoName} --bare`)
        await exec(`git -C ${directories.output + reposPath + "/" + gitRepoName} update-server-info`)
      }
      const location = directories.output + reposPath + "/" + gitRepoName
      await exec(`git -C ${location} symbolic-ref HEAD refs/heads/${repoConfig.defaultBranch}`)
    }
  }
}

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"))
  }

  eleventyConfig.addTemplateFormats("js")

  const slugify = eleventyConfig.getFilter("slugify")
  const reposData = await repos(reposConfiguration, eleventyConfig.dir.output, slugify)
  // 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 flatRels = getFlatRels(reposData)
  eleventyConfig.addGlobalData("flatRels", flatRels)

  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`
  const css = `${import.meta.dirname}/css`

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

  eleventyConfig.on(
    "eleventy.after",
    async ({ directories }) => {
      for (let repoName in reposConfiguration.repos) {
        const repoConfig = reposConfiguration.repos[repoName]

        if (typeof repoConfig.buildSteps !== 'undefined') {
          // make a temp directory for things to run in
          const tempDirName = `temp_${Math.floor(Math.random() * 10000).toString()}`
          const tempDir = `${directories.output.replace("./", "")}${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}`)
          const branchesAndTags = await getBranchesAndTags(repoConfig, repoName)
          const branchNames = branchesAndTags.branches.map(branch => branch.name)
          const tagNames = branchesAndTags.tags.map(tag => tag.name)
          for (let branch of branchNames) {
            // TODO why doesn't git -C checkout work? Says that repo doesn't exist
            await exec(`(cd ${tempDirRepoPath} && git checkout ${branch})`)
            for (let buildStep of repoConfig.buildSteps) {
              // Run the command for each step in each branch
              await exec(`(cd ${tempDirRepoPath} && ${buildStep.command})`)
              // Copy the specified folders from the "from" to the "to" dir
              await exec(`cp -r ${tempDirRepoPath}/${buildStep.copyFrom} ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branch)}/${buildStep.copyTo}`)
            }
          }
          for (let tag of tagNames) {
            await exec(`(cd ${tempDirRepoPath} && git checkout ${tag})`)
            for (let buildStep of repoConfig.buildSteps) {
              // Run the command for each step in each branch
              await exec(`(cd ${tempDirRepoPath} && ${buildStep.command})`)
              // Copy the specified folders from the "from" to the "to" dir
              await exec(`cp -r ${tempDirRepoPath}/${buildStep.copyFrom} ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}/tags/${eleventyConfig.getFilter("slugify")(tag)}/${buildStep.copyTo}`)
            }
          }
          // delete the temp dirs
          await exec(`rm -r ${tempDir}`)
        }
      }
    }
  )

  eleventyConfig.addFilter("getDirectoryContents", (repo: string, branch: string, dirPath: string) => {
    const fileList = reposData.find(
      current => current.name === repo
    ).branches.find(
      current => current.name === branch
    ).fileList

    return Array.from(fileList.keys()).filter(
      file => file.startsWith(dirPath) && file !== dirPath
    )
  })

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

  eleventyConfig.addFilter("lineNumbers", (code: string) => {
    code.split('')
    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, branchName: string) => {
    const renderer = eleventyConfig?.javascript?.functions?.renderContent
    if (renderer) {
      return await renderer.bind({
        data: {
          branch: branchName
        },
      })(contentString, "liquid,md")
    }
    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): SortedFileList => {
    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).sort((a, b) => {
      if (a.isDirectory && b.isDirectory) {
        return 0
      }
      if (a.isDirectory && !b.isDirectory) {
        return -1
      }
      return 1
    })

    return sortedByDirectory
  })

  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 = Array.from(files.keys()).some((testFile) => {
        return testFile.startsWith(filename + '/') && (testFile !== filename)
      })
    return isDirectory
  })

  eleventyConfig.addFilter("pagesJustForRel", (pages: Array<PatchPage>, repoName: string, relName: string, relType: "branch" | "tag") => {
    return pages.filter(page => page.repoName === repoName && page.relName === relName && page.type === relType)
  })

  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 slugify = eleventyConfig.getFilter("slugify")
    const location = getLocation(reposConfiguration, eleventyConfig.dir.output, repoName, slugify)
    // TODO allow for other filenames besides README.md
    const command = `git -C ${location} show ${branchName}:README.md`
    try {
      const res = await exec(command)
      return res.stdout
    } catch {
      return ""
    }
  })

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

  // INDEX.TS
  eleventyConfig.addTemplate(
    'repos/index.11ty.js',
    commonPage(indexJsTemplate, {}, eleventyConfig),
    {
      permalink: `${reposPath}/index.html`,
    }
  )

  // BRANCHES.TS
  eleventyConfig.addTemplate(
    'repos/branches.11ty.js',
    commonPage(branchesJsTemplate, reposConfiguration, eleventyConfig),
    {
      pagination: {
        data: "flatRels",
        size: 1,
        alias: "flatRel",
      },
      permalink: (data) => {
        const repoName = data.flatRel.repoName
        const relName = data.flatRel.relName
        const relType = data.flatRel.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(relName)}/branches/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.flatRel.repoName,
          relName: (data) => data.flatRel.relName,
          path: "branches"
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.flatRel.repoName
        }),
        currentRel: (data) => {
          const relType = data.flatRel.type === "branch" ? "branches" : "tags"
          const currentRepo = reposData.find(repo => {
            return repo.name === data.flatRel.repoName
          })
          return currentRepo[relType].find(rel => {
            return rel.name === data.flatRel.relName
          })
        }
      },
      navTab: "branches",
    }
  )

  // TAGS.TS
  eleventyConfig.addTemplate(
    'repos/tags.11ty.js',
    commonPage(tagsJsTemplate, reposConfiguration, eleventyConfig),
    {
      pagination: {
        data: "flatRels",
        size: 1,
        alias: "flatRel",
      },
      permalink: (data) => {
        const repoName = data.flatRel.repoName
        const relName = data.flatRel.relName
        const relType = data.flatRel.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(relName)}/tags/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.flatRel.repoName,
          relName: (data) => data.flatRel.relName,
          path: "tags"
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.flatRel.repoName
        }),
        currentRel: (data) => {
          const relType = data.flatRel.type === "branch" ? "branches" : "tags"
          const currentRepo = reposData.find(repo => {
            return repo.name === data.flatRel.repoName
          })
          return currentRepo[relType].find(rel => {
            return rel.name === data.flatRel.relName
          })
        }
      },
      navTab: "tags",
    }
  )

  // FILE.TS
  const flatFilesData = flatFiles(reposData)
  eleventyConfig.addTemplate(
    'repos/file.11ty.js',
    commonPage(fileJsTemplate, reposConfiguration, eleventyConfig),
    {
      pagination: {
        data: "flatFiles",
        size: 1,
        alias: "fileInfo",
      },
      flatFiles: flatFilesData,
      permalink: (data) => {
        const repoName = data.fileInfo.repoName
        const branchName = data.fileInfo.branchName
        const relType = data.fileInfo.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(branchName)}/files/${data.fileInfo.file.split('/').map((filePart) => filePart.split('.').map((subPart) => eleventyConfig.getFilter("slugify")(subPart)).join('.')).join('/')}.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
        }),
        currentRel: (data) => reposData.find(repo => {
          return repo.name === data.fileInfo.repoName
        }).branches.find(branch => {
          return branch.name === data.fileInfo.branchName
        }),
      },
      navTab: "files",
    }
  )

  // RAW.TS
  eleventyConfig.addTemplate(
    'repos/raw.11ty.js',
    rawJsTemplate(eleventyConfig),
    {
      pagination: {
        data: "flatFiles",
        size: 1,
        alias: "fileInfo",
      },
      eleventyAllowMissingExtension: true,
      flatFiles: flatFilesData,
      permalink: (data) => {
        const repoName = data.fileInfo.repoName
        const branchName = data.fileInfo.branchName
        const relType = data.fileInfo.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(branchName)}/raw/${data.fileInfo.file.split('.').map(filePart => eleventyConfig.getFilter("slugify")(filePart)).join('.')}`
      },
      eleventyComputed: {
        currentRel: (data) => reposData.find(repo => {
          return repo.name === data.fileInfo.repoName
        }).branches.find(branch => {
          return branch.name === data.fileInfo.branchName
        }),
      }
    }
  )

  // FILES.TS
  eleventyConfig.addTemplate(
    'repos/files.11ty.js',
    commonPage(filesJsTemplate, reposConfiguration, eleventyConfig),
    {
      pagination: {
        data: "flatRels",
        size: 1,
        alias: "flatRel",
      },
      permalink: (data) => {
        const repoName = data.flatRel.repoName
        const relName = data.flatRel.relName
        const relType = data.flatRel.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(relName)}/files/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.flatRel.repoName,
          relName: (data) => data.flatRel.relName,
          path: "files",
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.flatRel.repoName
        }),
        currentRel: (data) => {
          const relType = data.flatRel.type === "branch" ? "branches" : "tags"
          return reposData.find(repo => {
            return repo.name === data.flatRel.repoName
          })[relType].find(rel => {
            return rel.name === data.flatRel.relName
          })
        }
      },
      navTab: "files",
    }
  )

  // REPO.TS
  eleventyConfig.addTemplate(
    'repos/repo.11ty.js',
    commonPage(repoJsTemplate, reposConfiguration, eleventyConfig),
    {
      pagination: {
        data: "flatRels",
        size: 1,
        alias: "flatRel",
      },
      permalink: (data) => {
        const repoName = data.flatRel.repoName
        const relName = data.flatRel.relName
        const relType = data.flatRel.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(relName)}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.flatRel.repoName,
          relName: (data) => data.flatRel.relName,
          path: (data) => '', // TODO: ask about why empty string here shows up as a function
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.flatRel.repoName
        }),
        currentRel: (data) => {
          const relType = data.flatRel.type === "branch" ? "branches" : "tags"
          return reposData.find(repo => {
            return repo.name === data.flatRel.repoName
          })[relType].find(rel => {
            return rel.name === data.flatRel.relName
          })
        }
      },
      navTab: "home"
    }
  )

  // COMMITS.TS
  const paginatedPatchesData = await paginatedPatches(reposData)
  eleventyConfig.addTemplate(
    `repos/commits.11ty.js`,
    commonPage(commitsJsTemplate, reposConfiguration, eleventyConfig),
    {
      pagination: {
        data: "paginatedPatches",
        size: 1,
        alias: "patchPage",
      },
      paginatedPatches: paginatedPatchesData,
      permalink: (data) => {
        const repoName = data.patchPage.repoName
        const relName = data.patchPage.relName
        const relType = data.patchPage.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(relName)}/commits/page${data.patchPage.pageNumber}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.patchPage.repoName,
          relName: (data) => data.patchPage.relName,
          path: 'commits/page1',
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.patchPage.repoName
        }),
        currentRel: (data) => {
          const relType = data.patchPage.type === "branch" ? "branches" : "tags"
          return reposData.find(repo => {
            return repo.name === data.patchPage.repoName
          })[relType].find(rel => {
            return rel.name === data.patchPage.relName
          })
        }
      },
      navTab: "commits",
    }
  )

  // COMMIT.TS
  const flatPatchesData = await flatPatches(reposData)
  eleventyConfig.addTemplate(
    `repos/commit.11ty.js`,
    commonPage(commitJsTemplate, reposConfiguration, eleventyConfig),
    {
      pagination: {
        data: "flatPatches",
        size: 1,
        alias: "patchInfo",
      },
      flatPatches: flatPatchesData,
      permalink: (data) => {
        const repoName = data.patchInfo.repoName
        const relName = data.patchInfo.relName
        const relType = data.patchInfo.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(relName)}/commits/${data.patchInfo.commit.hash}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.patchInfo.repoName,
          relName: (data) => data.patchInfo.relName,
          path: 'commits/page1',
        },
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.patchInfo.repoName
        }),
        currentRel: (data) => {
          const relType = data.patchInfo.type === "branch" ? "branches" : "tags"
          return reposData.find(repo => {
            return repo.name === data.patchInfo.repoName
          })[relType].find(rel => {
            return rel.name === data.patchInfo.relName
          })
        },
      },
      navTab: "commits",
    }
  )

  // FEED.TS
  eleventyConfig.addTemplate(
    `repos/feed.11ty.js`,
    feedJsTemplate(eleventyConfig),
    {
      pagination: {
        data: "flatRels",
        size: 1,
        alias: "flatRel",
      },
      permalink: (data) => {
        const repoName = data.flatRel.repoName
        const relName = data.flatRel.relName
        const relType = data.flatRel.type
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/${relType}/${eleventyConfig.getFilter("slugify")(relName)}/commits.xml`
      },
      eleventyComputed: {
        currentRepo: (data) => reposData.find(repo => {
          return repo.name === data.flatRel.repoName
        }),
        currentRel: (data) => {
          const relType = data.flatRel.type === "branch" ? "branches" : "tags"
          return reposData.find(repo => {
            return repo.name === data.flatRel.repoName
          })[relType].find(rel => {
            return rel.name === data.flatRel.relName
          })
        },
        currentRelCommits: (data) => {
          return flatPatchesData.filter((patch) => {
            return patch.relName === data.flatRel.relName
          })
        }
      },
      eleventyExcludeFromCollections: true,
    }
  )
}
