/**
 * scaffold-profiles.js — profile resolution for ADR-022 scaffold profiles.
 *
 * Purpose: single entry point for resolving the active profile Set<string> from
 * all precedence sources: PREFERENCES.md frontmatter → manifest → auto-detection.
 * Also handles custom profile YAML files under ~/.sf/profiles/.
 *
 * Consumer: detectScaffoldDrift, ensureAgenticDocsScaffold, doc-checker, migrate.
 */
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import {
	PROFILE_NAMES,
	PROFILES,
	SCAFFOLD_FILES,
} from "./scaffold-constants.js";
import { sfHome } from "./sf-home.js";

const SCAFFOLD_FILE_PATHS = new Set(SCAFFOLD_FILES.map((f) => f.path));

/**
 * Read the `sf_profile:` field from PREFERENCES.md frontmatter.
 *
 * Purpose: allow per-repo, git-committed profile override that takes precedence
 * over the runtime manifest field. Returns null when the file is absent, has no
 * frontmatter, or has no `sf_profile` key.
 *
 * Consumer: resolveActiveProfileSet.
 */
export function readPreferencesProfile(basePath) {
	const prefsPath = join(basePath, "PREFERENCES.md");
	let content;
	try {
		content = readFileSync(prefsPath, "utf-8");
	} catch {
		return null;
	}
	if (!content.startsWith("---")) return null;
	const end = content.indexOf("\n---", 3);
	if (end === -1) return null;
	const block = content.slice(3, end);
	for (const line of block.split("\n")) {
		const colon = line.indexOf(":");
		if (colon === -1) continue;
		const key = line.slice(0, colon).trim();
		if (key !== "sf_profile") continue;
		const val = line
			.slice(colon + 1)
			.replace(/#.*$/, "")
			.trim()
			.replace(/^["']|["']$/g, "");
		return val || null;
	}
	return null;
}

/**
 * Load a custom profile YAML file from ~/.sf/profiles/<name>.yaml.
 *
 * Purpose: allow power-users to define repo-type profiles beyond the five
 * built-ins without forking SF. Returns null when the file does not exist.
 * Returns an error string (not a Set) when the file is malformed so callers
 * can surface a diagnostic instead of silently falling back.
 *
 * Schema:
 *   extends: <built-in profile name>   # required
 *   add:                               # optional — paths relative to repo root
 *     - docs/DESIGN.md
 *   remove:                            # optional
 *     - docs/exec-plans/active/index.md
 *
 * Consumer: resolveActiveProfileSet.
 */
export function loadCustomProfileSet(name) {
	const profilePath = join(sfHome(), "profiles", `${name}.yaml`);
	if (!existsSync(profilePath)) return null;
	let content;
	try {
		content = readFileSync(profilePath, "utf-8");
	} catch (err) {
		return `failed to read ${profilePath}: ${err.message}`;
	}
	// Minimal YAML parser: handles scalar "extends" and sequence "add"/"remove".
	const fields = { extends: null, add: [], remove: [] };
	let currentList = null;
	for (const rawLine of content.split("\n")) {
		const line = rawLine.replace(/#.*$/, ""); // strip comments
		if (!line.trim()) continue;
		// Sequence item
		if (/^\s+-\s/.test(line)) {
			const value = line
				.replace(/^\s+-\s*/, "")
				.trim()
				.replace(/^["']|["']$/g, "");
			if (currentList === "add") fields.add.push(value);
			else if (currentList === "remove") fields.remove.push(value);
			continue;
		}
		// Key: value
		const colon = line.indexOf(":");
		if (colon <= 0) continue;
		const key = line.slice(0, colon).trim();
		const val = line
			.slice(colon + 1)
			.trim()
			.replace(/^["']|["']$/g, "");
		if (key === "extends") {
			fields.extends = val || null;
			currentList = null;
		} else if (key === "add") {
			currentList = "add";
		} else if (key === "remove") {
			currentList = "remove";
		} else {
			currentList = null;
		}
	}
	if (!fields.extends) {
		return `custom profile '${name}' is missing required 'extends' field`;
	}
	if (!PROFILE_NAMES.includes(fields.extends)) {
		return `custom profile '${name}': extends '${fields.extends}' is not a built-in profile (${PROFILE_NAMES.join(", ")})`;
	}
	const base = new Set(PROFILES[fields.extends]);
	// Validate add paths.
	for (const p of fields.add) {
		if (!SCAFFOLD_FILE_PATHS.has(p)) {
			return `custom profile '${name}': add path '${p}' is not a known scaffold file`;
		}
		base.add(p);
	}
	// Validate remove paths.
	for (const p of fields.remove) {
		if (!SCAFFOLD_FILE_PATHS.has(p)) {
			return `custom profile '${name}': remove path '${p}' is not a known scaffold file`;
		}
		base.delete(p);
	}
	return base;
}

/**
 * Detect the most appropriate built-in profile for a repo.
 *
 * Purpose: prevent SF from laying down ~60-70% irrelevant scaffold files when
 * bootstrapping into infra/ops/docs repos that don't have a code-app shape.
 * Returns the profile name to store in the manifest and filter scaffold files.
 *
 * Heuristic (ADR-022 §4): checks for language/infra marker files in order of
 * specificity. Falls back to "app" to preserve existing behaviour for repos
 * that look like applications.
 *
 * Consumer: resolveActiveProfileSet (first-run path), ensureAgenticDocsScaffold.
 */
export function detectRepoProfile(basePath) {
	// Nix repos are almost always infrastructure.
	if (
		existsSync(join(basePath, "flake.nix")) ||
		existsSync(join(basePath, "shell.nix"))
	) {
		return "infra";
	}
	// Repos with a Terraform or Pulumi entry point.
	if (
		existsSync(join(basePath, "main.tf")) ||
		existsSync(join(basePath, "Pulumi.yaml")) ||
		existsSync(join(basePath, "Pulumi.yml"))
	) {
		return "infra";
	}
	// Pure docs repo: no source manifests, only markdown.
	const hasSource =
		existsSync(join(basePath, "package.json")) ||
		existsSync(join(basePath, "go.mod")) ||
		existsSync(join(basePath, "Cargo.toml")) ||
		existsSync(join(basePath, "pyproject.toml")) ||
		existsSync(join(basePath, "setup.py")) ||
		existsSync(join(basePath, "CMakeLists.txt"));
	if (!hasSource && existsSync(join(basePath, "docs"))) {
		return "docs";
	}
	// Node repos: check for UI framework dependency (→ app) vs library.
	if (existsSync(join(basePath, "package.json"))) {
		try {
			const pkg = JSON.parse(
				readFileSync(join(basePath, "package.json"), "utf-8"),
			);
			const allDeps = {
				...(pkg.dependencies ?? {}),
				...(pkg.devDependencies ?? {}),
			};
			const uiFrameworks = ["react", "vue", "svelte", "next", "@angular/core"];
			if (uiFrameworks.some((f) => allDeps[f])) return "app";
			return "library";
		} catch {
			return "app";
		}
	}
	// Go, Rust, Python → library by default (no frontend implied).
	if (
		existsSync(join(basePath, "go.mod")) ||
		existsSync(join(basePath, "Cargo.toml")) ||
		existsSync(join(basePath, "pyproject.toml"))
	) {
		return "library";
	}
	// Fallback: preserve existing behaviour.
	return "app";
}

/**
 * Resolve the active profile Set<string> for a given repo path.
 *
 * Purpose: single authoritative implementation of ADR-022 §6 precedence rules
 * so all callers (drift, scaffold, migrate, doctor) agree on the active profile.
 *
 * Precedence (highest first):
 *   1. PREFERENCES.md frontmatter `sf_profile:` (committed, explicit)
 *   2. `explicitProfile` argument (caller-supplied override, e.g. migrate --profile=)
 *   3. Manifest `profile` field (runtime, gitignored)
 *   4. Auto-detection via detectRepoProfile (first-run fallback)
 *
 * Returns `{ profileName, profileSet, warning? }`. `warning` is set when a
 * custom profile file is malformed or unknown — callers surface it as a notice,
 * never as a hard error.
 *
 * Consumer: detectScaffoldDrift, ensureAgenticDocsScaffold, doc-checker,
 * runScaffoldMigrate.
 */
export function resolveActiveProfileSet(basePath, manifest, explicitProfile) {
	// 1. PREFERENCES.md frontmatter — highest precedence.
	const prefsProfile = readPreferencesProfile(basePath);
	const resolved = prefsProfile ?? explicitProfile ?? manifest?.profile ?? null;
	if (!resolved) {
		// Auto-detect from filesystem heuristics (first-run fallback).
		const detected = basePath ? detectRepoProfile(basePath) : "app";
		return { profileName: detected, profileSet: PROFILES[detected] };
	}
	// Built-in?
	if (PROFILE_NAMES.includes(resolved)) {
		return { profileName: resolved, profileSet: PROFILES[resolved] };
	}
	// Custom profile?
	const custom = loadCustomProfileSet(resolved);
	if (custom === null) {
		// File doesn't exist — warn and fall back.
		return {
			profileName: "app",
			profileSet: PROFILES.app,
			warning: `custom profile '${resolved}' not found at ~/.sf/profiles/${resolved}.yaml — using 'app'`,
		};
	}
	if (typeof custom === "string") {
		// Malformed — warn and fall back.
		return {
			profileName: "app",
			profileSet: PROFILES.app,
			warning: `${custom} — using 'app'`,
		};
	}
	return { profileName: resolved, profileSet: custom };
}
