import fsImport from 'fs'
import util from 'util'
import childProcess from 'child_process'
import {repos, getBranchNames} 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 { 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 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, reposConfiguration) {
  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 fetchCommands = (await getBranchNames(repoConfig, repoName)).map(branch => `git -C ${location} fetch origin ${branch}:${branch}`).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 reposData = await repos(reposConfiguration, eleventyConfig.dir.output)
  // 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`
  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}`)
          for (let branch of await getBranchNames(repoConfig, repoName)) {
            // 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}`)
            }
          }
          // 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 lines = code.split('\n')
    const numLines = lines.length
    const lineNumbers = []
    let printedLineNumber = 0
    for (let i = 0; i < numLines - 1; i++) {
      if (lines[i].includes('\u{2063}')) {
        lineNumbers.push(null)
      }
      else {
        lineNumbers.push(printedLineNumber)
        printedLineNumber++
      }
    }

    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.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, eleventyConfig.dir.output, 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, eleventyConfig.dir.output, repo)
    const command = `git -C ${location} show ${branch}:${filename}`
    const res = await exec(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.addAsyncFilter("getReadMe", async (repoName: string, branchName: string) => {
    const location = getLocation(reposConfiguration, eleventyConfig.dir.output, repoName)
    // 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: "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)}/branches/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.branchInfo.repoName,
          branchName: (data) => data.branchInfo.branchName,
          path: "branches"
        },
        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.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
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${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
        }),
        currentBranch: (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
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/raw/${data.fileInfo.file.split('.').map(filePart => eleventyConfig.getFilter("slugify")(filePart)).join('.')}`
      },
    }
  )

  // FILES.TS
  eleventyConfig.addTemplate(
    'repos/files.11ty.js',
    commonPage(filesJsTemplate, reposConfiguration, eleventyConfig),
    {
      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.TS
  eleventyConfig.addTemplate(
    'repos/repo.11ty.js',
    commonPage(repoJsTemplate, reposConfiguration, eleventyConfig),
    {
      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: (data) => '', // ask about why empty string here shows up as a function
        },
        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: "home"
    }
  )

  // COMMITS.TS
  const paginatedPatchesData = await paginatedPatches(reposData)
  eleventyConfig.addTemplate(
    `repos/patches.11ty.js`,
    commonPage(commitsJsTemplate, reposConfiguration, eleventyConfig),
    {
      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)}/commits/page${data.patchPage.pageNumber}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.patchPage.repoName,
          branchName: (data) => data.patchPage.branchName,
          path: 'commits/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: "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 branchName = data.patchInfo.branchName
        return `${reposPath}/${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branchName)}/commits/${data.patchInfo.commit.hash}/`
      },
      eleventyComputed: {
        nav: {
          repoName: (data) => data.patchInfo.repoName,
          branchName: (data) => data.patchInfo.branchName,
          path: 'commits/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
        }),
      },
      navTab: "commits",
    }
  )

  // FEED.TS
  eleventyConfig.addTemplate(
    `repos/feed.11ty.js`,
    feedJsTemplate(eleventyConfig),
    {
      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)}/commits.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
        }),
        currentBranchCommits: (data) => {
          return flatPatchesData.filter((patch) => {
            return patch.branchName === data.branch.branchName
          })
        }
      },
      eleventyExcludeFromCollections: true,
    }
  )
}
