/* eslint-disable no-restricted-globals */
import { spawn } from 'node:child_process'
import { createHash } from 'node:crypto'
import * as fs from 'node:fs'
import path from 'node:path'
import { Readable } from 'node:stream'
import { fileURLToPath } from 'node:url'

import * as glob from 'glob'

import { getCurrentBranch, getCurrentCommit } from '../../scripts/git-utils.js'

const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))

const GITHUB_TOKEN = process.env.GITHUB_TOKEN
let SKIP_PREBUILT = process.env.BUILD_FOR_DOCS === '1'

if (!GITHUB_TOKEN && !SKIP_PREBUILT) {
    console.warn('GITHUB_TOKEN is required to publish crypto-node, skipping prebuilt artifacts')
    SKIP_PREBUILT = true
}

const GITHUB_HEADERS = {
    'Authorization': `Bearer ${GITHUB_TOKEN}`,
    'Content-Type': 'application/json',
    'X-GitHub-Api-Version': '2022-11-28',
}
const API_PREFIX = 'https://api.github.com/repos/mtcute/mtcute/actions/workflows/node-prebuilt.yaml'
const PLATFORMS = ['ubuntu', 'macos', 'windows']

async function findArtifactsByHash(hash) {
    const runs = await fetch(`${API_PREFIX}/runs?per_page=100`, { headers: GITHUB_HEADERS }).then(r => r.json())

    for (const run of runs.workflow_runs) {
        if (run.conclusion !== 'success' || run.status !== 'completed') continue

        const artifacts = await fetch(`${run.url}/artifacts`, { headers: GITHUB_HEADERS })
            .then(r => r.json())
            .then(r => r.artifacts)

        for (const it of artifacts) {
            if (it.expired) continue
            const parts = it.name.split('-')

            if (parts[0] === 'prebuilt'
              && PLATFORMS.includes(parts[1])
              && parts[3] === hash) {
                return artifacts
            }
        }
    }

    return null
}

async function runWorkflow(commit, hash) {
    const createRes = await fetch(`${API_PREFIX}/dispatches`, {
        method: 'POST',
        headers: GITHUB_HEADERS,
        body: JSON.stringify({
            ref: getCurrentBranch(),
            inputs: { commit, hash },
        }),
    })

    if (createRes.status !== 204) {
        const text = await createRes.text()
        throw new Error(`Failed to run workflow: ${createRes.status} ${text}`)
    }

    // wait for the workflow to finish
    // github api is awesome and doesn't return the run id, so let's just assume it's the last one
    await new Promise(resolve => setTimeout(resolve, 5000))
    const runsRes = await fetch(`${API_PREFIX}/runs`, {
        headers: GITHUB_HEADERS,
    }).then(r => r.json())

    let run = runsRes.workflow_runs[0]

    while (run.status === 'queued' || run.status === 'in_progress') {
        await new Promise(resolve => setTimeout(resolve, 5000))
        run = await fetch(run.url, { headers: GITHUB_HEADERS }).then(r => r.json())
    }

    if (run.status !== 'completed') {
        throw new Error(`Workflow ${run.id} failed: ${run.status}`)
    }

    if (run.conclusion !== 'success') {
        throw new Error(`Workflow ${run.id} failed: ${run.conclusion}`)
    }

    // fetch artifacts
    const artifacts = await fetch(`${run.url}/artifacts`, { headers: GITHUB_HEADERS })
        .then(r => r.json())
        .then(r => r.artifacts)

    // validate their names
    for (const it of artifacts) {
        const parts = it.name.split('-')

        if (parts[0] !== 'prebuilt'
          || !PLATFORMS.includes(parts[1])
          || parts[3] !== hash) {
            throw new Error(`Invalid artifact name: ${it.name}`)
        }
    }

    return artifacts
}

async function extractArtifacts(artifacts) {
    fs.mkdirSync(path.join(__dirname, 'dist/prebuilds'), { recursive: true })
    await Promise.all(
        artifacts.map(async (it) => {
            const platform = it.name.split('-').slice(1, 3).join('-')

            const res = await fetch(it.archive_download_url, {
                headers: GITHUB_HEADERS,
                redirect: 'manual',
            })

            if (res.status !== 302) {
                const text = await res.text()
                throw new Error(`Failed to download artifact ${it.name}: ${res.status} ${text}`)
            }

            const zip = await fetch(res.headers.get('location'))

            const outFile = path.join(__dirname, 'dist/prebuilds', `${platform}.zip`)
            const stream = fs.createWriteStream(outFile)

            await new Promise((resolve, reject) => {
                stream.on('finish', resolve)
                stream.on('error', reject)
                Readable.fromWeb(zip.body).pipe(stream)
            })

            // extract the zip
            await new Promise((resolve, reject) => {
                const child = spawn('unzip', [outFile, '-d', path.join(__dirname, 'dist/prebuilds')], {
                    stdio: 'inherit',
                })

                child.on('exit', (code) => {
                    if (code !== 0) {
                        reject(new Error(`Failed to extract ${outFile}: ${code}`))
                    } else {
                        resolve()
                    }
                })
            })
            fs.unlinkSync(outFile)
        }),
    )
}

/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => ({
    async finalize({ packageDir, outDir }) {
        const libDir = path.resolve(packageDir, 'lib')

        if (!SKIP_PREBUILT) {
            // generate sources hash
            const hashes = []

            for (const file of glob.sync(path.join(libDir, '**/*'))) {
                const hash = createHash('sha256')
                hash.update(fs.readFileSync(file))
                hashes.push(hash.digest('hex'))
            }

            const hash = createHash('sha256')
                .update(hashes.join('\n'))
                .digest('hex')
            console.log(hash)

            console.log('[i] Checking for prebuilt artifacts for %s', hash)
            let artifacts = await findArtifactsByHash(hash)

            if (!artifacts) {
                console.log('[i] No artifacts found, running workflow')
                artifacts = await runWorkflow(getCurrentCommit(), hash)
            }

            console.log('[i] Extracting artifacts')
            await extractArtifacts(artifacts)
        }

        // copy native sources and binding.gyp file

        fs.cpSync(libDir, path.join(outDir, 'lib'), { recursive: true })

        const bindingGyp = fs.readFileSync(path.join(packageDir, 'binding.gyp'), 'utf8')
        fs.writeFileSync(
            path.join(outDir, 'binding.gyp'),
            bindingGyp
                // replace paths to crypto
                .replace(/"\.\.\/crypto/g, '"crypto'),
        )

        // for some unknown fucking reason ts doesn't do this
        fs.copyFileSync(path.join(packageDir, 'src/native.cjs'), path.join(outDir, 'native.cjs'))
    },
})
