/**
 * trust.mjs — driver trust enforcement (TOFU lock + policy file + decision).
 *
 * Implements ID-03:
 *   - 4 trust tiers: first_party_signed (default-allow), community_verified,
 *     community_unverified, local_unverified.
 *   - Default policy: only first_party_signed auto-runs.
 *   - Non-first-party drivers require one-time user approval; approval is
 *     bound to a TOFU lock at .aperant/driver-tofu.json keyed by
 *     {driver_id, version, manifest_sha256, artifact_digest}. Any drift
 *     re-prompts.
 *
 * The audit log (audit.mjs) writes one line per decision (allow/deny).
 * The execution layer (exec.mjs) enforces structured-only local_command.
 *
 * This module is decision-only: it does NOT prompt the user. The caller
 * (workflow runner / apt-tools driver doctor) decides whether to invoke
 * the prompt UX, then calls recordApproval() to persist the TOFU entry.
 */

import { createHash } from 'node:crypto'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'

/**
 * @typedef {import('@aperant/framework/driver-sdk').DriverManifest} DriverManifest
 */

const TOFU_PATH = '.aperant/driver-tofu.json'
const POLICY_PATH = '.aperant/policy.json'

/**
 * Compute a stable manifest hash for TOFU + audit lines.
 * @param {DriverManifest} manifest
 * @returns {string}
 */
export function manifestSha256(manifest) {
	const stable = JSON.stringify(manifest, Object.keys(manifest).sort())
	return createHash('sha256').update(stable).digest('hex')
}

/**
 * Read the TOFU lock file. Returns an empty object if missing or malformed.
 * @param {string} projectDir
 * @returns {Record<string, {manifest_sha256: string, artifact_digest?: string, approved_at: string, trust_tier: string}>}
 */
export function readTofuLock(projectDir) {
	const path = join(resolve(projectDir), TOFU_PATH)
	if (!existsSync(path)) return {}
	try {
		return JSON.parse(readFileSync(path, 'utf-8'))
	} catch {
		return {}
	}
}

/**
 * Persist a TOFU lock file (atomic-ish: write + rename not used since we
 * append-only to a tiny JSON; conflicting writes from parallel agents
 * would clobber an entry but `.aperant/driver-tofu.json` is per-project,
 * not per-agent).
 *
 * @param {string} projectDir
 * @param {Record<string, unknown>} tofu
 */
export function writeTofuLock(projectDir, tofu) {
	const path = join(resolve(projectDir), TOFU_PATH)
	mkdirSync(dirname(path), { recursive: true })
	writeFileSync(path, JSON.stringify(tofu, null, 2) + '\n', 'utf-8')
}

/**
 * Build the canonical TOFU key for a (driver, version) pair. The hash is
 * NOT part of the key — the value's manifest_sha256 + artifact_digest
 * detect drift.
 * @param {string} driverId
 * @param {string} version
 * @returns {string}
 */
export function tofuKey(driverId, version) {
	return `${driverId}@${version}`
}

/**
 * Read the project's policy file. Returns the default policy if missing.
 * @param {string} projectDir
 * @returns {{mode: 'first_party_only' | 'allowlist', allowed: string[], denied: string[]}}
 */
export function readPolicy(projectDir) {
	const path = join(resolve(projectDir), POLICY_PATH)
	const defaults = { mode: 'first_party_only', allowed: [], denied: [] }
	if (!existsSync(path)) return defaults
	try {
		const parsed = JSON.parse(readFileSync(path, 'utf-8'))
		return {
			mode: parsed.mode === 'allowlist' ? 'allowlist' : 'first_party_only',
			allowed: Array.isArray(parsed.allowed) ? parsed.allowed : [],
			denied: Array.isArray(parsed.denied) ? parsed.denied : [],
		}
	} catch {
		return defaults
	}
}

/**
 * Decide whether a driver invocation is allowed.
 *
 * @param {DriverManifest} manifest
 * @param {ReturnType<typeof readTofuLock>} tofu
 * @param {ReturnType<typeof readPolicy>} policy
 * @returns {{decision: 'allow' | 'deny' | 'requires_approval', reason: string, expected_manifest_sha256?: string}}
 */
export function decide(manifest, tofu, policy) {
	const tier = manifest.trust?.tier
	const driverId = manifest.driverId
	const version = manifest.version

	// Policy denylist always wins.
	if (policy.denied.includes(driverId)) {
		return { decision: 'deny', reason: `policy denies driver ${driverId}` }
	}

	// first_party_signed: default-allow under either policy mode (unless
	// explicitly denied above).
	if (tier === 'first_party_signed') {
		return { decision: 'allow', reason: 'first_party_signed driver — default allow' }
	}

	// first_party_only mode blocks every other tier.
	if (policy.mode === 'first_party_only') {
		return {
			decision: 'deny',
			reason: `policy.mode=first_party_only blocks ${tier} driver`,
		}
	}

	// allowlist mode: require explicit allow entry AND a valid TOFU lock.
	if (!policy.allowed.includes(driverId)) {
		return {
			decision: 'deny',
			reason: `policy.mode=allowlist but ${driverId} not in allowed[]`,
		}
	}

	// Check TOFU lock.
	const key = tofuKey(driverId, version)
	const entry = tofu[key]
	const currentHash = manifestSha256(manifest)

	if (!entry) {
		return {
			decision: 'requires_approval',
			reason: `no TOFU entry for ${key} — first run requires approval`,
			expected_manifest_sha256: currentHash,
		}
	}

	if (entry.manifest_sha256 !== currentHash) {
		return {
			decision: 'requires_approval',
			reason: `TOFU manifest_sha256 drift for ${key} — driver changed since approval, re-prompt required`,
			expected_manifest_sha256: currentHash,
		}
	}

	return { decision: 'allow', reason: `TOFU entry valid for ${key}` }
}

/**
 * Persist a one-time approval for a (driver, version) pair into the TOFU
 * lock. The caller is responsible for actually prompting the user; this
 * function just writes the entry.
 *
 * @param {string} projectDir
 * @param {DriverManifest} manifest
 */
export function recordApproval(projectDir, manifest) {
	const tofu = readTofuLock(projectDir)
	const key = tofuKey(manifest.driverId, manifest.version)
	tofu[key] = {
		manifest_sha256: manifestSha256(manifest),
		artifact_digest: manifest.trust?.integrity?.artifact_digest,
		approved_at: new Date().toISOString(),
		trust_tier: manifest.trust?.tier,
	}
	writeTofuLock(projectDir, tofu)
}
