#!/usr/bin/env node
import { execFileSync } from "node:child_process"
import { mkdirSync, writeFileSync } from "node:fs"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"

const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = join(__dirname, "..", "..")
const baseVersion = process.env.BASE_VERSION ?? "1.0.0"
const outputDir = process.env.RELEASE_OUTPUT_DIR
  ? join(repoRoot, process.env.RELEASE_OUTPUT_DIR)
  : join(repoRoot, ".ci", "release")
const repository = process.env.GITEA_REPOSITORY ?? process.env.GITHUB_REPOSITORY ?? ""
const serverUrl = (process.env.GITEA_SERVER_URL ?? "https://git.shx.one").replace(/\/$/, "")

const MANIFEST_COMMIT_LIMIT = Number(process.env.RELEASE_MANIFEST_COMMIT_LIMIT ?? 6)
const CONVENTIONAL_RE =
  /^(feat|fix|chore|docs|refactor|style|test|build|ci)(\([^)]+\))?!?:\s*(.+)$/i
const MINOR_TYPES = new Set(["feat"])
const PATCH_TYPES = new Set([
  "fix",
  "chore",
  "docs",
  "refactor",
  "style",
  "test",
  "build",
  "ci",
])

function git(...args) {
  return execFileSync("git", args, {
    cwd: repoRoot,
    encoding: "utf8",
    stdio: ["ignore", "pipe", "pipe"],
  }).trim()
}

function parseSemver(version) {
  const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version)
  if (!match) {
    throw new Error(`Invalid semver: ${version}`)
  }
  return {
    major: Number(match[1]),
    minor: Number(match[2]),
    patch: Number(match[3]),
  }
}

function formatSemver(parts) {
  return `${parts.major}.${parts.minor}.${parts.patch}`
}

function bumpVersion(version, bump) {
  const parts = parseSemver(version)
  if (bump === "minor") {
    parts.minor += 1
    parts.patch = 0
    return formatSemver(parts)
  }
  parts.patch += 1
  return formatSemver(parts)
}

function parseCommit(subject, body = "") {
  const match = CONVENTIONAL_RE.exec(subject.trim())
  const breaking =
    /BREAKING CHANGE/i.test(body) ||
  (match?.[1] && subject.includes(`${match[1]}!:`))

  if (!match) {
    return { type: "other", scope: null, description: subject.trim(), bump: "patch" }
  }

  const type = match[1].toLowerCase()
  const scope = match[2]?.slice(1, -1) ?? null
  const description = match[3].trim()
  let bump = "patch"

  if (breaking || MINOR_TYPES.has(type)) {
    bump = "minor"
  } else if (PATCH_TYPES.has(type)) {
    bump = "patch"
  }

  return { type, scope, description, bump }
}

function getAllTags() {
  try {
    return git("tag", "--list", "v*.*.*", "--sort=-v:refname")
      .split("\n")
      .map((line) => line.trim())
      .filter(Boolean)
  } catch {
    return []
  }
}

function getPreviousTag(tag) {
  const tags = getAllTags()
  const index = tags.indexOf(tag)
  return index >= 0 && index + 1 < tags.length ? tags[index + 1] : null
}

function getTagPublishedAt(tag) {
  try {
    return git("log", "-1", "--format=%aI", tag)
  } catch {
    return null
  }
}

function toManifestCommits(commits) {
  return commits.map((commit) => ({
    sha: commit.sha,
    shortSha: commit.shortSha,
    subject: commit.subject,
    author: commit.author,
    type: commit.type,
  }))
}

function writeManifest(manifest) {
  mkdirSync(outputDir, { recursive: true })
  writeFileSync(join(outputDir, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
}

function getLatestTag() {
  return getAllTags()[0] ?? null
}

function getCommitsSince(tag) {
  const range = tag ? `${tag}..HEAD` : "HEAD"
  const raw = git(
    "log",
    range,
    "--pretty=format:%H%x1f%h%x1f%an%x1f%s%x1f%b%x1e",
  )
  if (!raw) return []

  return parseCommitLog(raw)
}

function getRecentCommits(limit = MANIFEST_COMMIT_LIMIT) {
  const raw = git(
    "log",
    "-n",
    String(limit),
    "HEAD",
    "--pretty=format:%H%x1f%h%x1f%an%x1f%s%x1f%b%x1e",
  )
  if (!raw) return []

  return parseCommitLog(raw)
}

function parseCommitLog(raw) {
  return raw
    .split("\x1e")
    .map((entry) => entry.trim())
    .filter(Boolean)
    .map((entry) => {
      const [sha, shortSha, author, subject, body] = entry.split("\x1f")
      const parsed = parseCommit(subject, body)
      return {
        sha,
        shortSha,
        author,
        subject,
        type: parsed.type,
        scope: parsed.scope,
        bump: parsed.bump,
      }
    })
}

function groupCommits(commits) {
  const groups = new Map()
  for (const commit of commits) {
    const key = commit.type
    if (!groups.has(key)) groups.set(key, [])
    groups.get(key).push(commit)
  }
  return groups
}

function renderReleaseNotes({ tag, version, commits }) {
  const lines = [`# ${tag}`, "", `Версия **${version}**.`, ""]
  const groups = groupCommits(commits)
  const order = ["feat", "fix", "chore", "docs", "refactor", "style", "test", "build", "ci", "other"]

  for (const type of order) {
    const items = groups.get(type)
    if (!items?.length) continue
    lines.push(`## ${type}`, "")
    for (const item of items) {
      lines.push(`- ${item.subject} (\`${item.shortSha}\`, ${item.author})`)
    }
    lines.push("")
  }

  return `${lines.join("\n").trim()}\n`
}

function buildReleaseUrl(tag) {
  if (!repository) return ""
  const [owner, repo] = repository.split("/")
  if (!owner || !repo) return ""
  return `${serverUrl}/${owner}/${repo}/releases/tag/${encodeURIComponent(tag)}`
}

function writeGithubOutput(values) {
  const outputFile = process.env.GITHUB_OUTPUT
  if (!outputFile) return
  const lines = Object.entries(values)
    .map(([key, value]) => `${key}=${String(value).replace(/\r?\n/g, "%0A")}`)
    .join("\n")
  writeFileSync(outputFile, `${lines}\n`, { encoding: "utf8", flag: "a" })
}

function main() {
  const latestTag = getLatestTag()
  const baseFromTag = latestTag?.replace(/^v/, "") ?? baseVersion
  const commits = getCommitsSince(latestTag)

  if (commits.length === 0) {
    const deployTag = latestTag ?? `v${baseFromTag}`
    const deployVersion = baseFromTag
    const displayCommits = getRecentCommits()
    const publishedAt = latestTag ? getTagPublishedAt(latestTag) : null

    mkdirSync(outputDir, { recursive: true })
    writeFileSync(join(outputDir, "release_notes.md"), "", "utf8")
    writeManifest({
      version: deployVersion,
      tag: deployTag,
      publishedAt,
      releaseUrl: latestTag ? buildReleaseUrl(latestTag) : "",
      commits: toManifestCommits(displayCommits),
    })

    writeGithubOutput({
      should_release: "false",
      version: deployVersion,
      tag: deployTag,
      bump: "none",
      release_url: latestTag ? buildReleaseUrl(latestTag) : "",
    })
    return
  }

  const bump = commits.some((commit) => commit.bump === "minor") ? "minor" : "patch"
  const version = bumpVersion(baseFromTag, bump)
  const tag = `v${version}`
  const publishedAt = new Date().toISOString()
  const releaseUrl = buildReleaseUrl(tag)
  const releaseNotes = renderReleaseNotes({ tag, version, commits })
  const manifest = {
    version,
    tag,
    publishedAt,
    releaseUrl,
    commits: toManifestCommits(getRecentCommits()),
  }

  mkdirSync(outputDir, { recursive: true })
  writeFileSync(join(outputDir, "release_notes.md"), releaseNotes, "utf8")
  writeManifest(manifest)

  writeGithubOutput({
    should_release: "true",
    version,
    tag,
    bump,
    release_url: releaseUrl,
  })

  process.stdout.write(`${tag}\n`)
}

main()
