/**
 * install/install-from-source.mjs — Bundle D part 1.
 *
 * `apt-tools install-from-source <project-dir>` for the Aperant-contributor
 * self-host case. Resolves the workspace `@aperant/framework` chain — packs
 * the framework itself AND every `workspace:*` dependency (none today after
 * the 0.8.5 driver-sdk inline, but the walker is general). Hands the full
 * tarball list to a single `npm install --prefix` invocation so any future
 * unpublished-pair case stops blowing up with `EUNSUPPORTEDPROTOCOL`.
 *
 * Stable envelope:
 *   { status: 'ok',     tarballs: string[], kernel_path: string }
 *   { status: 'error',  error: string, ...optional context }
 *
 * Test seams: `APT_PNPM_BIN` + `APT_NPM_BIN` mirror the existing
 * `install-kernel.mjs` + worker patterns.
 */

import { spawnSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, isAbsolute, join, resolve } from 'node:path'
import { err, ok } from '../util/result.mjs'
import { findWorkspaceFrameworkVersion } from './find-workspace-framework-version.mjs'
import { KERNEL_BIN_REL_PATH } from './install-kernel.mjs'

/**
 * Locate the monorepo root given the workspace framework package.json path.
 * `<root>/packages/framework/package.json` → `<root>`.
 *
 * @param {string} frameworkPkgPath
 * @returns {string}
 */
function monorepoRootFrom(frameworkPkgPath) {
	// `<root>/packages/framework/package.json` — go up THREE levels.
	return dirname(dirname(dirname(frameworkPkgPath)))
}

/**
 * Scan `<monorepoRoot>/packages/` for a directory whose `package.json:name`
 * matches `pkgName`. Returns the directory path (containing package.json),
 * or null if no match.
 *
 * @param {string} monorepoRoot
 * @param {string} pkgName
 * @returns {string|null}
 */
function findWorkspacePackage(monorepoRoot, pkgName) {
	const packagesDir = join(monorepoRoot, 'packages')
	if (!existsSync(packagesDir)) return null
	let entries
	try {
		entries = readdirSync(packagesDir, { withFileTypes: true })
	} catch {
		return null
	}
	for (const entry of entries) {
		if (!entry.isDirectory()) continue
		const candidate = join(packagesDir, entry.name, 'package.json')
		if (!existsSync(candidate)) continue
		try {
			const pkg = JSON.parse(readFileSync(candidate, 'utf-8'))
			if (pkg?.name === pkgName) return join(packagesDir, entry.name)
		} catch {
			// Skip unreadable package.json files.
		}
	}
	return null
}

/**
 * pnpm-pack a workspace package into `tmpDir` and return the resulting
 * tarball path. Uses `pnpm --filter <name> pack --pack-destination <tmp>`
 * so workspace protocols resolve to concrete versions inside the tarball.
 *
 * `knownBefore` is the set of .tgz filenames already present in `tmpDir`
 * before this call; the mtime fallback uses it to find only the NEW file,
 * avoiding coarse-timestamp ambiguity when multiple packs land in the same
 * directory.
 *
 * @param {string} monorepoRoot
 * @param {string} pkgName
 * @param {string} tmpDir
 * @param {Set<string>} knownBefore  Filenames (not paths) present before this pack.
 * @returns {{ tarball: string } | { error: string }}
 */
function packWorkspace(monorepoRoot, pkgName, tmpDir, knownBefore) {
	const pnpmBin = process.env.APT_PNPM_BIN || 'pnpm'
	const result = spawnSync(pnpmBin, ['--filter', pkgName, 'pack', '--pack-destination', tmpDir], {
		cwd: monorepoRoot,
		stdio: ['ignore', 'pipe', 'pipe'],
		encoding: 'utf-8',
		timeout: 120_000,
	})
	if (result.error) {
		return { error: `pnpm spawn failed for ${pkgName}: ${result.error.message}` }
	}
	if (result.status !== 0) {
		const stderr = (result.stderr || '').trim().slice(-600)
		return { error: `pnpm pack ${pkgName} exited with ${result.status}: ${stderr || 'no stderr'}` }
	}
	// pnpm pack prints the resulting tarball path on stdout. Take the last
	// `.tgz` line (pnpm sometimes emits warnings above the path line).
	const tarballLine = (result.stdout || '')
		.split('\n')
		.map((s) => s.trim())
		.reverse()
		.find((s) => s.endsWith('.tgz'))
	if (tarballLine) {
		// `tarballLine` may be a bare filename (older pnpm) or an absolute path.
		// Use `isAbsolute` so Windows backslash + drive-letter paths are also
		// recognized — `startsWith('/')` was POSIX-only.
		const tarball = isAbsolute(tarballLine) ? tarballLine : join(tmpDir, tarballLine)
		// SEC-001: containment check — tarball must resolve to a child of tmpDir.
		const resolvedTmpDir = resolve(tmpDir)
		const resolvedTarball = resolve(tarball)
		if (!resolvedTarball.startsWith(`${resolvedTmpDir}/`) && resolvedTarball !== resolvedTmpDir) {
			return { error: `pnpm pack ${pkgName}: tarball path escapes tmpDir: ${tarball}` }
		}
		return { tarball }
	}
	// Fall back to scanning tmpDir for the NEW tgz when stdout was
	// uninformative (some pnpm builds print only progress to stderr).
	// Use `knownBefore` to find only files added by this pack, avoiding
	// mtime coarse-timestamp ambiguity on FAT32/network filesystems.
	try {
		for (const f of readdirSync(tmpDir)) {
			if (!f.endsWith('.tgz')) continue
			if (knownBefore.has(f)) continue
			return { tarball: join(tmpDir, f) }
		}
	} catch {
		// fall through
	}
	return {
		error: `pnpm pack ${pkgName}: no .tgz produced (stdout: ${result.stdout?.slice(-200)})`,
	}
}

/**
 * `apt-tools install-from-source <project-dir>` — pack the workspace
 * framework + every `workspace:*` dep, then npm-install the full tarball
 * list into `<project-dir>/.aperant/deps/`.
 *
 * @param {string} projectDir
 * @returns {import('../util/result.mjs').CommandResult}
 */
export function cmdInstallFromSource(projectDir) {
	if (!projectDir) {
		return err('Usage: apt-tools install-from-source <project-dir>')
	}
	const targetDir = resolve(projectDir)

	const workspace = findWorkspaceFrameworkVersion(targetDir)
	if (!workspace) {
		return err('install-from-source: no @aperant/framework workspace found above project-dir')
	}
	const monorepoRoot = monorepoRootFrom(workspace.source_path)

	// QUA-001: re-read the workspace package.json with a try/catch — the file
	// was validated by findWorkspaceFrameworkVersion but could change between
	// the two reads (active monorepo, contributor machine).
	let frameworkPkg
	try {
		frameworkPkg = JSON.parse(readFileSync(workspace.source_path, 'utf-8'))
	} catch (e) {
		return err(
			`install-from-source: failed to read ${workspace.source_path}: ${e instanceof Error ? e.message : String(e)}`,
		)
	}

	// Enumerate workspace:* deps from dependencies + peerDependencies.
	const depSources = [frameworkPkg.dependencies, frameworkPkg.peerDependencies]
	const workspaceDeps = []
	for (const block of depSources) {
		if (!block || typeof block !== 'object') continue
		for (const [name, spec] of Object.entries(block)) {
			if (typeof spec === 'string' && spec.startsWith('workspace:')) {
				if (!workspaceDeps.includes(name)) workspaceDeps.push(name)
			}
		}
	}

	// Resolve each workspace dep to its `packages/<dir>` path. Reject hard
	// when any dep cannot be located — we cannot pack what we cannot find.
	const missingDeps = []
	const resolvedDeps = []
	for (const depName of workspaceDeps) {
		const pkgDir = findWorkspacePackage(monorepoRoot, depName)
		if (!pkgDir) {
			missingDeps.push(depName)
			continue
		}
		resolvedDeps.push({ name: depName, dir: pkgDir })
	}
	if (missingDeps.length > 0) {
		return err(
			`install-from-source: workspace:* dep(s) not found in packages/: ${missingDeps.join(', ')}`,
		)
	}

	// PRF-001: include framework in the same pack loop — eliminates structural
	// duplication and ensures per-pack knownBefore snapshots work uniformly.
	const packTargets = [...resolvedDeps.map((d) => d.name), '@aperant/framework']

	// Pack everything into a fresh tmp dir so accumulated stale tarballs
	// from prior runs don't sneak in.
	const tmpDir = mkdtempSync(join(tmpdir(), 'apt-install-from-source-'))
	const tarballs = []

	// QUA-002: wrap pack+install in try/finally so tmpDir is always removed.
	try {
		for (const pkgName of packTargets) {
			// LOG-003: snapshot existing tgz filenames before each pack so the
			// fallback can identify exactly the new file, even on coarse-timestamp
			// filesystems (FAT32, some network mounts).
			const knownBefore = new Set()
			try {
				for (const f of readdirSync(tmpDir)) {
					if (f.endsWith('.tgz')) knownBefore.add(f)
				}
			} catch {
				// tmpDir unreadable — proceed with empty set; fallback will still work.
			}
			const packResult = packWorkspace(monorepoRoot, pkgName, tmpDir, knownBefore)
			if ('error' in packResult) return err(packResult.error)
			tarballs.push(packResult.tarball)
		}

		// npm install --prefix .aperant/deps <tarball1> <tarball2> ...
		const depsDir = join(targetDir, '.aperant', 'deps')
		mkdirSync(depsDir, { recursive: true })

		const npmBin = process.env.APT_NPM_BIN || 'npm'
		const args = [
			'install',
			'--prefix',
			depsDir,
			...tarballs,
			'--no-save',
			'--no-package-lock',
			'--no-audit',
			'--no-fund',
			'--silent',
		]
		const npmResult = spawnSync(npmBin, args, {
			cwd: targetDir,
			stdio: ['ignore', 'pipe', 'pipe'],
			encoding: 'utf-8',
			timeout: 180_000,
		})
		if (npmResult.error) {
			return err(`install-from-source: npm spawn failed: ${npmResult.error.message}`)
		}
		if (npmResult.status !== 0) {
			const stderr = (npmResult.stderr || '').trim().slice(-600)
			return err(
				`install-from-source: npm install --prefix exited with ${npmResult.status}: ${stderr || 'no stderr'}`,
			)
		}

		const kernelPath = join(targetDir, KERNEL_BIN_REL_PATH)
		if (!existsSync(kernelPath)) {
			return err(`install-from-source: npm reported success but kernel not found at ${kernelPath}`)
		}

		return ok({
			status: 'ok',
			command: 'install-from-source',
			tarballs,
			kernel_path: kernelPath,
		})
	} finally {
		rmSync(tmpDir, { recursive: true, force: true })
	}
}
