import type { EditableTreeNode } from 'vue-router/unplugin'
import type { Changelog, Contributor, GitLogFileEntry, GitLogOptions } from '../types'
import path from 'node:path'
import process from 'node:process'
import consola from 'consola'
import fs from 'fs-extra'
import { git } from '.'
import { shouldIncludeCommit } from '../types'
import { createContributor, deduplicateContributors, resolveContributorsGitHub } from './contributor'

/**
 * ASCII Unit Separator — used as field delimiter in git pretty formats.
 * Unlike `|`, this character never appears in normal text (author names,
 * emails, commit messages), eliminating parse collisions.
 */
const FIELD_SEP = '\x1F'
const RE_WHITESPACE = /\s+/

interface FrontmatterWithGitLog {
  git_log: Record<string, any>
}

function getFrontmatter(route: EditableTreeNode): FrontmatterWithGitLog {
  return route.meta.frontmatter as FrontmatterWithGitLog
}

/** Extract the login from a `https://github.com/<login>` URL. */
function extractLoginFromGithubUrl(url: string): string | undefined {
  return url.split('/').pop() || undefined
}

/**
 * Reconstruct an `email -> login` map from a previously written `git-log.json`.
 * Lets a committed cache seed GitHub resolution so already-known emails skip
 * the API entirely (only brand-new emails are queried).
 */
function buildKnownLoginsFromCache(prebuiltData: GitLogFileEntry): Map<string, string> {
  const knownLogins = new Map<string, string>()
  for (const entry of Object.values(prebuiltData)) {
    for (const contributor of entry.contributors || []) {
      if (!contributor.email || !contributor.github || knownLogins.has(contributor.email))
        continue
      const login = extractLoginFromGithubUrl(contributor.github)
      if (login)
        knownLogins.set(contributor.email, login)
    }
  }
  return knownLogins
}

/**
 * Serialize an `email -> login` map to stable JSON (keys sorted, trailing
 * newline) so the committed cache produces minimal, conflict-free diffs.
 */
export function serializeLoginCache(logins: Record<string, string>): string {
  const sorted: Record<string, string> = {}
  for (const email of Object.keys(logins).sort())
    sorted[email] = logins[email]
  return `${JSON.stringify(sorted, null, 2)}\n`
}

/**
 * Read the committed `email -> login` cache. Missing or malformed files
 * degrade gracefully to an empty map.
 */
async function readLoginCache(filePath: string): Promise<Record<string, string>> {
  try {
    if (await fs.pathExists(filePath))
      return JSON.parse(await fs.readFile(filePath, 'utf-8'))
  }
  catch (error) {
    consola.error('valaxy-addon-git-log: Error reading GitHub login cache:', error)
  }
  return {}
}

/**
 * Merge freshly resolved logins into the existing cache and write it back.
 * Skips writing when there is nothing to persist.
 */
async function writeLoginCache(
  filePath: string,
  existing: Record<string, string>,
  contributors: Contributor[],
): Promise<void> {
  const merged: Record<string, string> = { ...existing }
  for (const contributor of contributors) {
    if (!contributor.github)
      continue
    const login = extractLoginFromGithubUrl(contributor.github)
    if (login)
      merged[contributor.email] = login
  }

  if (!Object.keys(merged).length)
    return

  try {
    await fs.mkdir(path.dirname(filePath), { recursive: true })
    await fs.writeFile(filePath, serializeLoginCache(merged), 'utf-8')
  }
  catch (error) {
    consola.error('valaxy-addon-git-log: Error writing GitHub login cache at', filePath, error)
  }
}

export const destDir = path.resolve(process.cwd(), './public')
// Only allow files from the user's working directory 'pages' folder
export const currentWorkingDirectory = path.join(process.cwd(), 'pages')

/**
 * basePath is resolved asynchronously via `git revparse`. Store the promise
 * so that callers can `await` it instead of racing against `setBasePath`.
 */
let basePathPromise: Promise<string> | undefined
let basePath: string | undefined

export function initBasePath() {
  basePathPromise = git.revparse(['--show-toplevel']).then((result) => {
    basePath = result.trim()
    return basePath
  }).catch((error) => {
    consola.warn('valaxy-addon-git-log: Could not resolve git root, falling back to cwd.', error)
    basePath = process.cwd()
    return basePath
  })
  return basePathPromise
}

export async function ensureBasePath(): Promise<string> {
  if (basePath)
    return basePath
  if (basePathPromise)
    return basePathPromise
  return initBasePath()
}

/** @deprecated Use ensureBasePath() instead */
export function setBasePath(p: string) {
  basePath = p
}

export function getBasePath() {
  return basePath
}

/**
 * Pending route info collected during extendRoute, processed in batch later.
 */
interface PendingRoute {
  route: EditableTreeNode
  filePath: string
  gitRelativePath: string
}

let pendingRoutes: PendingRoute[] = []

/**
 * Collect route info during extendRoute (fast, no git calls).
 * Actual git operations are deferred to `flushGitLogBatch`.
 */
export async function handleGitLogInfo(options: GitLogOptions, route: EditableTreeNode) {
  const strategy = options.contributor?.strategy
  const isPrebuilt = strategy === 'prebuilt'
  const isBuildTime = strategy === 'build-time'

  const filePath = route.components.get('default')
  if (!filePath)
    return

  const frontmatter = getFrontmatter(route)
  if (!frontmatter.git_log)
    frontmatter.git_log = {}

  // Ensure basePath is available before computing relative path
  let resolvedBase: string
  try {
    resolvedBase = await ensureBasePath()
  }
  catch {
    // Fallback: use cwd-relative path
    resolvedBase = process.cwd()
  }

  const gitRelativePath = path.relative(resolvedBase, filePath).split(path.sep).join('/')
  frontmatter.git_log.path = gitRelativePath

  if (!isPrebuilt && !isBuildTime)
    return

  if (!filePath.startsWith(currentWorkingDirectory))
    return

  pendingRoutes.push({ route, filePath, gitRelativePath })
}

/**
 * Batch-fetch contributors for all files in a single git command.
 * Returns a map of filePath -> Contributor[].
 */
async function batchGetContributors(resolvedBase: string, filePaths: string[], options?: GitLogOptions): Promise<Map<string, Contributor[]>> {
  const result = new Map<string, Contributor[]>()
  if (!filePaths.length)
    return result

  const { contributor } = options || {}

  try {
    const gitArgs = [
      'log',
      '--no-merges',
      `--pretty=format:---COMMIT_SEP---%an${FIELD_SEP}%ae`,
      '--name-only',
      ...(contributor?.logArgs?.trim() ? contributor.logArgs.trim().split(RE_WHITESPACE) : []),
      '--',
      ...filePaths,
    ]

    const raw = await git.raw(gitArgs)

    // Parse: each block is "---COMMIT_SEP---author\x1femail\nfile1\nfile2\n..."
    const blocks = raw.split('---COMMIT_SEP---').filter(Boolean)

    // fileContribMap: filePath -> { email -> Contributor }
    const fileContribMap = new Map<string, Record<string, Contributor>>()

    for (const block of blocks) {
      const lines = block.trim().split('\n')
      if (!lines.length)
        continue

      const [name, email] = lines[0].split(FIELD_SEP)
      if (!email)
        continue

      const files = lines.slice(1).filter(Boolean)
      for (const file of files) {
        // Resolve to absolute path for matching
        const absPath = path.resolve(resolvedBase, file)
        if (!fileContribMap.has(absPath))
          fileContribMap.set(absPath, {})

        const contribs = fileContribMap.get(absPath)!
        if (!contribs[email]) {
          contribs[email] = createContributor(name, email)
        }
        contribs[email].count++
      }
    }

    for (const [fp, contribs] of fileContribMap) {
      const entries = Object.values(contribs)
      result.set(fp, deduplicateContributors(entries).sort((a, b) => b.count - a.count))
    }
  }
  catch (e) {
    consola.error('valaxy-addon-git-log: Error batch-fetching contributors:', e)
  }

  return result
}

/**
 * Batch-fetch changelogs for all files in a single git command.
 * Returns a map of filePath -> Changelog[].
 */
async function batchGetChangelog(resolvedBase: string, filePaths: string[], maxCount: number, options?: GitLogOptions): Promise<Map<string, Changelog[]>> {
  const result = new Map<string, Changelog[]>()
  if (!filePaths.length)
    return result

  try {
    // `git log --max-count` with multiple pathspecs limits the *global* commit
    // count, not per-file. We still need a global cap to keep the command
    // bounded on large repos — use `maxCount * filePaths.length` as a heuristic
    // upper bound. Each file's array is then truncated to `maxCount` in JS
    // below to preserve per-file semantics.
    const totalCap = Math.max(maxCount, maxCount * filePaths.length)
    const raw = await git.raw([
      'log',
      '--name-only',
      `--max-count=${totalCap}`,
      `--pretty=format:---CL_SEP---%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%aI${FIELD_SEP}%s`,
      '--',
      ...filePaths,
    ])

    const blocks = raw.split('---CL_SEP---').filter(Boolean)

    for (const block of blocks) {
      const lines = block.trim().split('\n')
      if (!lines.length)
        continue

      const headerLine = lines[0]
      const [hash, authorName, authorEmail, date, ...rest] = headerLine.split(FIELD_SEP)
      const message = rest.join(FIELD_SEP) || ''

      if (!shouldIncludeCommit(message, options?.changelog))
        continue

      const log: Changelog = {
        hash,
        date,
        message,
        refs: '',
        author_name: authorName,
        author_email: authorEmail,
      }

      if (message.includes('chore: release')) {
        log.version = message.split(' ')[2]?.trim()
      }

      const files = lines.slice(1).filter(Boolean)
      for (const file of files) {
        const absPath = path.resolve(resolvedBase, file)
        if (!result.has(absPath))
          result.set(absPath, [])
        result.get(absPath)!.push(log)
      }
    }
  }
  catch (e) {
    consola.error('valaxy-addon-git-log: Error batch-fetching changelogs:', e)
  }

  // Truncate each file's changelog to maxCount to match per-file semantics
  for (const [fp, logs] of result) {
    if (logs.length > maxCount)
      result.set(fp, logs.slice(0, maxCount))
  }

  return result
}

/**
 * Process all pending routes in batch: 2 git commands for ALL files
 * instead of 2 × N git commands (one per file).
 */
export async function flushGitLogBatch(options: GitLogOptions) {
  if (!pendingRoutes.length)
    return

  const routes = pendingRoutes
  pendingRoutes = []

  const strategy = options.contributor?.strategy
  const isPrebuilt = strategy === 'prebuilt'
  const isBuildTime = strategy === 'build-time'

  let resolvedBase: string
  try {
    resolvedBase = await ensureBasePath()
  }
  catch (error) {
    consola.error('valaxy-addon-git-log: Failed to resolve git root in flushGitLogBatch, skipping.', error)
    return
  }

  const filePaths = routes.map(r => r.filePath)
  const maxCount = options.changelog?.maxCount ?? (process.env.CI ? 1000 : 100)

  // 2 git commands for ALL files (instead of 2 × N)
  const [contributorsMap, changelogMap] = await Promise.all([
    batchGetContributors(resolvedBase, filePaths, options),
    batchGetChangelog(resolvedBase, filePaths, maxCount, options),
  ])

  // Load the existing prebuilt cache up-front so we can both reuse previously
  // resolved GitHub logins (below) and merge per-file entries (later).
  const gitLogPath = isPrebuilt && destDir ? path.join(destDir, 'git-log.json') : undefined
  let prebuiltData: GitLogFileEntry = {}

  if (gitLogPath) {
    try {
      if (await fs.pathExists(gitLogPath))
        prebuiltData = JSON.parse(await fs.readFile(gitLogPath, 'utf-8'))
    }
    catch (error) {
      consola.error('valaxy-addon-git-log: Error reading existing git log file:', error)
    }
  }

  // Resolve GitHub usernames for contributors without noreply emails.
  // Seed from both the dedicated login cache and any prior git-log.json so a
  // committed cache lets the build run with zero (or minimal) GitHub API calls.
  if (options.repositoryUrl && options.contributor?.resolveGitHub !== false) {
    const githubCachePath = options.contributor?.githubCache
      ? path.resolve(process.cwd(), options.contributor.githubCache)
      : undefined
    const cachedLogins = githubCachePath ? await readLoginCache(githubCachePath) : {}

    const knownLogins = buildKnownLoginsFromCache(prebuiltData)
    for (const [email, login] of Object.entries(cachedLogins)) {
      if (!knownLogins.has(email))
        knownLogins.set(email, login)
    }

    const allContributors = [...new Set([...contributorsMap.values()].flat())]
    await resolveContributorsGitHub(allContributors, options.repositoryUrl, knownLogins)

    // Persist resolved logins back to the small, committable cache.
    if (githubCachePath)
      await writeLoginCache(githubCachePath, cachedLogins, allContributors)
  }

  for (const { route, filePath, gitRelativePath } of routes) {
    const contributors = contributorsMap.get(filePath) || []
    const changeLog = changelogMap.get(filePath) || []

    if (isBuildTime) {
      const frontmatter = getFrontmatter(route)
      frontmatter.git_log.contributors = contributors
      frontmatter.git_log.changeLog = changeLog
    }

    if (isPrebuilt) {
      prebuiltData[gitRelativePath] = {
        contributors,
        changeLog,
        path: gitRelativePath,
      }
    }
  }

  if (gitLogPath) {
    try {
      await fs.mkdir(path.dirname(gitLogPath), { recursive: true })
      await fs.writeFile(gitLogPath, JSON.stringify(prebuiltData, null, 2), 'utf-8')
    }
    catch (error) {
      consola.error('valaxy-addon-git-log: Error writing git log file at', gitLogPath, error)
    }
  }
}
