/**
 * sf-learning plugin — entry point
 *
 * Wires together the four S01-S04 modules into a single registerable plugin:
 *
 *   loadCapabilityOverrides → priors (per (unit_type, model))
 *   sf-db outcome writer    → write llm_task_outcomes rows
 *   outcome-aggregator      → rolling-window observed stats
 *   bayesian-blender        → α · prior + (1-α) · observed + UCB1
 *   hook-handler            → translates the above into a before_model_select handler
 *
 * ## Usage
 *
 *   import { init } from "./index.mjs";
 *   const plugin = await init(pi, {
 *     priorsPath: "./src/data/model-benchmarks.json",
 *     weightsPath: "./src/data/unit-weights.json",
 *     nPrior: 10,
 *     rollingDays: 30,
 *     explorationC: 1.4,
 *   });
 *
 *   // plugin.recordOutcome({...}) on unit completion
 *   // plugin.unregister() on tear down
 *
 * ## Side effects
 * - Uses the already-open `.sf/sf.db`, or opens `<basePath>/.sf/sf.db`
 * - Relies on the shared SF DB bootstrap for `llm_task_outcomes`
 * - Registers a hook on the supplied pi instance
 *
 * ## Errors
 * - Init failures are wrapped with a stage label so callers can see where
 *   things broke ("loading priors", "opening db", "registering hook")
 * - Once init succeeds, the running handler is fire-and-forget — it cannot
 *   crash the dispatch path
 *
 * @module sf-learning
 */

import { mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import {
	getDatabase,
	insertLlmTaskOutcome,
	openDatabase as openSfDatabase,
} from "../sf-db.js";
import { writeFallbackChains } from "./fallback-chain-writer.mjs";
import {
	createBeforeModelSelectHandler,
	registerBeforeModelSelect,
} from "./hook-handler.mjs";
import { loadCapabilityOverrides } from "./loadCapabilityOverrides.mjs";
import { aggregateAllForUnitType } from "./outcome-aggregator.mjs";

const DEFAULT_N_PRIOR = 10;
const DEFAULT_ROLLING_DAYS = 30;
const DEFAULT_EXPLORATION_C = 1.4;
const DEFAULT_DB_SUBPATH = [".sf", "sf.db"];
const HOME_REGEX = /^~(?=$|\/)/;

/**
 * @typedef {Object} PluginConfig
 * @property {string} [basePath]            - default: process.cwd()
 * @property {string} [dbPath]              - default: <basePath>/.sf/sf.db
 * @property {string} [priorsPath]          - default: <plugin>/data/model-benchmarks.json
 * @property {string} [weightsPath]         - default: <plugin>/data/unit-weights.json
 * @property {number} [nPrior=10]
 * @property {number} [rollingDays=30]
 * @property {number} [explorationC=1.4]
 * @property {boolean} [explorationEnabled=true]
 * @property {Object} [db]                  - pre-opened db handle for hook reads
 * @property {(msg: string) => void} [log]
 */

/**
 * @typedef {Object} PluginInstance
 * @property {() => void}                   unregister
 * @property {(outcome: Object) => boolean} recordOutcome
 * @property {() => Promise<void>}          reloadPriors
 * @property {Object}                       deps
 */

/**
 * Expand a leading `~` to the user's home directory.
 *
 * @param {string} path
 * @returns {string}
 */
function expandPath(path) {
	if (typeof path !== "string") return path;
	return path.replace(HOME_REGEX, homedir());
}

/**
 * Resolve the shared SF database path for learning state.
 *
 * @returns {string}
 */
function defaultDbPath(config) {
	return join(config.basePath ?? process.cwd(), ...DEFAULT_DB_SUBPATH);
}

/**
 * Resolve the learning outcomes database from SF's shared database handle.
 *
 * Purpose: keep UOK outcome learning, model stats, and learned routing on one
 * `.sf/sf.db` ledger instead of splitting feedback into a sidecar database.
 *
 * @param {PluginConfig} config
 * @returns {Object} duck-typed sqlite handle
 */
function openDatabase(config) {
	if (config.db) {
		return config.db;
	}

	const activeDb = getDatabase();
	if (activeDb) {
		return activeDb;
	}

	const dbPath = expandPath(config.dbPath ?? defaultDbPath(config));
	if (dbPath !== ":memory:") {
		mkdirSync(dirname(dbPath), { recursive: true });
	}
	if (!openSfDatabase(dbPath)) {
		throw new Error(`failed to open shared SF database at ${dbPath}`);
	}
	const db = getDatabase();
	if (!db) {
		throw new Error(`shared SF database did not become available at ${dbPath}`);
	}
	return db;
}

/**
 * Build the dependency bundle the hook handler consumes.
 *
 * @param {Object} db
 * @param {{overrides: Object, weights: Object, benchmarks: Object}} priors
 * @param {PluginConfig} config
 * @returns {import("./hook-handler.mjs").HookDeps}
 */
function buildHookDeps(db, priors, config) {
	return {
		db,
		overrides: priors.overrides,
		weights: priors.weights,
		benchmarks: priors.benchmarks,
		opts: {
			nPrior: config.nPrior ?? DEFAULT_N_PRIOR,
			ucbC: config.explorationC ?? DEFAULT_EXPLORATION_C,
			rollingDays: config.rollingDays ?? DEFAULT_ROLLING_DAYS,
			explorationEnabled: config.explorationEnabled !== false,
			log: config.log,
		},
	};
}

/**
 * Wrap a thrown error with a stage label so callers can see which init
 * step failed.
 *
 * @param {string} stage
 * @param {unknown} err
 * @returns {Error}
 */
function wrapInitError(stage, err) {
	const message = err instanceof Error ? err.message : String(err);
	const wrapped = new Error(
		`sf-learning init failed at stage "${stage}": ${message}`,
	);
	if (err instanceof Error && err.stack) {
		wrapped.stack = `${wrapped.message}\nCaused by: ${err.stack}`;
	}
	return wrapped;
}

/**
 * Initialize the plugin: load priors, resolve shared db, register hook.
 *
 * @param {Object} pi
 * @param {PluginConfig} [config={}]
 * @returns {Promise<PluginInstance>}
 */
export async function init(pi, config = {}) {
	let priors;
	try {
		priors = await loadCapabilityOverrides({
			benchmarksPath: config.priorsPath,
			weightsPath: config.weightsPath,
		});
	} catch (err) {
		throw wrapInitError("loading priors", err);
	}

	let db;
	try {
		db = await openDatabase(config);
	} catch (err) {
		throw wrapInitError("opening db", err);
	}

	const deps = buildHookDeps(db, priors, config);

	let unregister;
	try {
		unregister = registerBeforeModelSelect(pi, deps);
	} catch (err) {
		throw wrapInitError("registering hook", err);
	}

	// Regenerate pi-ai runtime fallback chains (read by FallbackResolver).
	// Writes ~/.sf/agent/settings.json → fallback.chains.* atomically.
	// Failure is logged but never blocks plugin init — stale chains are
	// still better than a broken plugin.
	let fallbackWriteSummary = null;
	if (config.fallbackSettingsPath && config.writeFallbackChains !== false) {
		try {
			fallbackWriteSummary = writeFallbackChains(
				config.fallbackSettingsPath,
				deps,
				{
					blackoutModels: config.blackoutModels ?? [],
				},
			);
			config.log?.(
				`wrote ${fallbackWriteSummary.chainsWritten} fallback chain(s) ` +
					`(${fallbackWriteSummary.totalEntries} total entries) to ${config.fallbackSettingsPath}`,
			);
		} catch (err) {
			config.log?.(
				`fallback chain write failed (non-fatal): ${err?.message ?? String(err)}`,
			);
		}
	}

	return {
		unregister,
		fallbackWriteSummary,
		recordOutcome: (outcome) => insertLlmTaskOutcome(outcome),
		reloadPriors: async () => {
			const fresh = await loadCapabilityOverrides({
				benchmarksPath: config.priorsPath,
				weightsPath: config.weightsPath,
			});
			deps.overrides = fresh.overrides;
			deps.weights = fresh.weights;
			deps.benchmarks = fresh.benchmarks;
		},
		deps,
	};
}

/**
 * Convenience: create a handler without registering it. Useful for tests
 * and for users who want to wire the hook themselves.
 *
 * @param {import("./hook-handler.mjs").HookDeps} deps
 * @returns {(hookInput: Object) => Promise<{modelId: string} | undefined>}
 */
export function createHandler(deps) {
	return createBeforeModelSelectHandler(deps);
}

export {
	aggregateAllForUnitType,
	loadCapabilityOverrides,
	recordOutcome,
	registerBeforeModelSelect,
};
