import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
import { spawn } from "node:child_process";
import * as os from "node:os";
import { dirname, join, resolve } from "node:path";
import type { ExtensionAPI, ExtensionContext, ToolResultEvent } from "@mariozechner/pi-coding-agent";

const HOOK_TIMEOUT_MS = 10 * 60 * 1000;
const LOG_FILE = join(os.homedir(), ".pi", "hooks-resolution.log");

type HookEventName = "PostToolUse" | "PostToolUseFailure";

type ResolvedCommandHook = {
	eventName: HookEventName;
	matcher?: RegExp;
	matcherText?: string;
	command: string;
	source: string;
};

type HookState = {
	projectDir: string;
	hooks: ResolvedCommandHook[];
};

type CommandRunResult = {
	code: number;
	stdout: string;
	stderr: string;
	elapsedMs: number;
	timedOut: boolean;
};

function logHook(message: string): void {
	const line = `[${new Date().toISOString()}] ${message}`;
	console.error(`[hooks-resolution] ${line}`);

	try {
		mkdirSync(dirname(LOG_FILE), { recursive: true });
		appendFileSync(LOG_FILE, `${line}\n`);
	} catch {
		// ignore logging failures
	}
}

function isFile(path: string): boolean {
	try {
		return statSync(path).isFile();
	} catch {
		return false;
	}
}

function asRecord(value: unknown): Record<string, unknown> | undefined {
	if (typeof value !== "object" || value === null) {
		return undefined;
	}
	return value as Record<string, unknown>;
}

function walkUpDirectories(startDir: string, stopDir?: string): string[] {
	const directories: string[] = [];
	const hasStopDir = stopDir !== undefined;
	let current = resolve(startDir);
	let parent = dirname(current);
	let reachedStopDir = hasStopDir && current === stopDir;
	let reachedFilesystemRoot = parent === current;

	directories.push(current);
	while (!reachedStopDir && !reachedFilesystemRoot) {
		current = parent;
		parent = dirname(current);
		reachedStopDir = hasStopDir && current === stopDir;
		reachedFilesystemRoot = parent === current;
		directories.push(current);
	}

	return directories;
}

function findNearestGitRoot(startDir: string): string | undefined {
	for (const directory of walkUpDirectories(startDir)) {
		if (existsSync(join(directory, ".git"))) {
			return directory;
		}
	}
	return undefined;
}

function hasHooksConfig(directory: string): boolean {
	const claudeSettingsPath = join(directory, ".claude", "settings.json");
	const ruleSyncHooksPath = join(directory, ".rulesync", "hooks.json");
	const piHooksPath = join(directory, ".pi", "hooks.json");
	return isFile(claudeSettingsPath) || isFile(ruleSyncHooksPath) || isFile(piHooksPath);
}

function findProjectDir(cwd: string): string {
	const gitRoot = findNearestGitRoot(cwd);
	for (const directory of walkUpDirectories(cwd, gitRoot)) {
		if (hasHooksConfig(directory)) {
			return directory;
		}
	}

	return gitRoot ?? resolve(cwd);
}

function readJsonFile(path: string): unknown | undefined {
	if (!isFile(path)) {
		return undefined;
	}

	try {
		return JSON.parse(readFileSync(path, "utf8")) as unknown;
	} catch {
		return undefined;
	}
}

function resolveHookCommand(command: string, projectDir: string): string {
	return command.replace(/\$CLAUDE_PROJECT_DIR\b/g, projectDir);
}

function compileMatcher(matcherText: string | undefined): RegExp | undefined {
	if (matcherText === undefined) {
		return undefined;
	}

	try {
		return new RegExp(matcherText);
	} catch {
		return undefined;
	}
}

function createHook(
	eventName: HookEventName,
	matcherText: string | undefined,
	command: string,
	source: string,
	projectDir: string,
): ResolvedCommandHook | undefined {
	const matcher = compileMatcher(matcherText);
	if (matcherText !== undefined && matcher === undefined) {
		return undefined;
	}

	return {
		eventName,
		matcher,
		matcherText,
		command: resolveHookCommand(command, projectDir),
		source,
	};
}

function getHookEntries(
	hooksRecord: Record<string, unknown>,
	eventName: HookEventName,
): unknown[] {
	const keys =
		eventName === "PostToolUse"
			? ["PostToolUse", "postToolUse"]
			: ["PostToolUseFailure", "postToolUseFailure"];

	for (const key of keys) {
		const value = hooksRecord[key];
		if (Array.isArray(value)) {
			return value;
		}
	}

	return [];
}

function parseClaudeSettingsHooks(
	config: unknown,
	source: string,
	projectDir: string,
): ResolvedCommandHook[] {
	const root = asRecord(config);
	const hooksRoot = root ? asRecord(root.hooks) : undefined;
	if (!hooksRoot) {
		return [];
	}

	const hooks: ResolvedCommandHook[] = [];
	const events: HookEventName[] = ["PostToolUse", "PostToolUseFailure"];

	for (const eventName of events) {
		const entries = getHookEntries(hooksRoot, eventName);
		for (const entry of entries) {
			const entryRecord = asRecord(entry);
			if (!entryRecord || !Array.isArray(entryRecord.hooks)) {
				continue;
			}

			const matcherText =
				typeof entryRecord.matcher === "string" ? entryRecord.matcher : undefined;
			for (const nestedHook of entryRecord.hooks) {
				const nestedHookRecord = asRecord(nestedHook);
				if (!nestedHookRecord) {
					continue;
				}

				if (nestedHookRecord.type !== "command") {
					continue;
				}

				if (typeof nestedHookRecord.command !== "string") {
					continue;
				}

				const hook = createHook(
					eventName,
					matcherText,
					nestedHookRecord.command,
					source,
					projectDir,
				);
				if (hook) {
					hooks.push(hook);
				}
			}
		}
	}

	return hooks;
}

function parseSimpleHooksFile(
	config: unknown,
	source: string,
	projectDir: string,
): ResolvedCommandHook[] {
	const root = asRecord(config);
	const hooksRoot = root ? asRecord(root.hooks) : undefined;
	if (!hooksRoot) {
		return [];
	}

	const hooks: ResolvedCommandHook[] = [];
	const events: HookEventName[] = ["PostToolUse", "PostToolUseFailure"];

	for (const eventName of events) {
		const entries = getHookEntries(hooksRoot, eventName);
		for (const entry of entries) {
			const entryRecord = asRecord(entry);
			if (!entryRecord || typeof entryRecord.command !== "string") {
				continue;
			}

			const matcherText =
				typeof entryRecord.matcher === "string" ? entryRecord.matcher : undefined;
			const hook = createHook(
				eventName,
				matcherText,
				entryRecord.command,
				source,
				projectDir,
			);
			if (hook) {
				hooks.push(hook);
			}
		}
	}

	return hooks;
}

function loadHooks(cwd: string): HookState {
	const projectDir = findProjectDir(cwd);
	const claudeSettingsPath = join(projectDir, ".claude", "settings.json");
	const ruleSyncHooksPath = join(projectDir, ".rulesync", "hooks.json");
	const piHooksPath = join(projectDir, ".pi", "hooks.json");

	const hooks: ResolvedCommandHook[] = [];

	const claudeSettings = readJsonFile(claudeSettingsPath);
	if (claudeSettings !== undefined) {
		hooks.push(...parseClaudeSettingsHooks(claudeSettings, claudeSettingsPath, projectDir));
	}

	const ruleSyncHooks = readJsonFile(ruleSyncHooksPath);
	if (ruleSyncHooks !== undefined) {
		hooks.push(...parseSimpleHooksFile(ruleSyncHooks, ruleSyncHooksPath, projectDir));
	}

	const piHooks = readJsonFile(piHooksPath);
	if (piHooks !== undefined) {
		hooks.push(...parseSimpleHooksFile(piHooks, piHooksPath, projectDir));
	}

	return {
		projectDir,
		hooks,
	};
}

function toClaudeToolName(toolName: string): string {
	if (toolName === "ls") {
		return "LS";
	}
	if (toolName.length === 0) {
		return toolName;
	}
	return toolName[0].toUpperCase() + toolName.slice(1);
}

function matchesHook(hook: ResolvedCommandHook, toolName: string): boolean {
	if (!hook.matcher) {
		return true;
	}

	const claudeToolName = toClaudeToolName(toolName);
	hook.matcher.lastIndex = 0;
	if (hook.matcher.test(toolName)) {
		return true;
	}

	hook.matcher.lastIndex = 0;
	return hook.matcher.test(claudeToolName);
}

function extractTextContent(content: unknown): string {
	if (!Array.isArray(content)) {
		return "";
	}

	const parts: string[] = [];
	for (const item of content) {
		if (!item || typeof item !== "object") {
			continue;
		}

		const itemRecord = item as Record<string, unknown>;
		if (itemRecord.type === "text" && typeof itemRecord.text === "string") {
			parts.push(itemRecord.text);
		}
	}

	return parts.join("\n");
}

function normalizeToolInput(input: Record<string, unknown>): Record<string, unknown> {
	const normalized: Record<string, unknown> = { ...input };
	const pathValue = typeof input.path === "string" ? input.path : undefined;

	if (pathValue !== undefined) {
		normalized.file_path = pathValue;
		normalized.filePath = pathValue;
	}

	return normalized;
}

function buildToolResponse(
	event: ToolResultEvent,
	normalizedInput: Record<string, unknown>,
): Record<string, unknown> {
	const response: Record<string, unknown> = {
		is_error: event.isError,
		isError: event.isError,
		content: event.content,
		text: extractTextContent(event.content),
		details: event.details ?? null,
	};

	const filePath =
		typeof normalizedInput.file_path === "string" ? normalizedInput.file_path : undefined;
	if (filePath !== undefined) {
		response.file_path = filePath;
		response.filePath = filePath;
	}

	return response;
}

function buildHookPayload(
	event: ToolResultEvent,
	eventName: HookEventName,
	ctx: ExtensionContext,
	projectDir: string,
): Record<string, unknown> {
	const normalizedInput = normalizeToolInput(event.input);
	const sessionId = ctx.sessionManager.getSessionFile() ?? "ephemeral";

	return {
		session_id: sessionId,
		cwd: ctx.cwd,
		claude_project_dir: projectDir,
		hook_event_name: eventName,
		tool_name: toClaudeToolName(event.toolName),
		tool_call_id: event.toolCallId,
		tool_input: normalizedInput,
		tool_response: buildToolResponse(event, normalizedInput),
	};
}

function runCommandHook(
	command: string,
	cwd: string,
	payload: Record<string, unknown>,
): Promise<CommandRunResult> {
	return new Promise((resolve) => {
		const startedAt = Date.now();
		const child = spawn("bash", ["-lc", command], {
			cwd,
			env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
			stdio: ["pipe", "pipe", "pipe"],
		});

		let stdout = "";
		let stderr = "";
		let timedOut = false;
		let resolved = false;

		const finish = (code: number) => {
			if (resolved) {
				return;
			}
			resolved = true;
			resolve({
				code,
				stdout,
				stderr,
				elapsedMs: Date.now() - startedAt,
				timedOut,
			});
		};

		const timeout = setTimeout(() => {
			timedOut = true;
			child.kill("SIGTERM");

			const killTimer = setTimeout(() => {
				child.kill("SIGKILL");
			}, 1000);
			(killTimer as NodeJS.Timeout & { unref?: () => void }).unref?.();
		}, HOOK_TIMEOUT_MS);
		(timeout as NodeJS.Timeout & { unref?: () => void }).unref?.();

		child.stdout.on("data", (chunk: Buffer) => {
			stdout += chunk.toString("utf8");
		});
		child.stderr.on("data", (chunk: Buffer) => {
			stderr += chunk.toString("utf8");
		});

		child.on("error", (error) => {
			clearTimeout(timeout);
			stderr += `${error.message}\n`;
			finish(-1);
		});

		child.on("close", (code) => {
			clearTimeout(timeout);
			finish(code ?? -1);
		});

		try {
			child.stdin.write(JSON.stringify(payload));
			child.stdin.end();
		} catch (error) {
			const message = error instanceof Error ? error.message : String(error);
			stderr += `${message}\n`;
		}
	});
}

function hookEventNameForResult(event: ToolResultEvent): HookEventName {
	return event.isError ? "PostToolUseFailure" : "PostToolUse";
}

export default function(pi: ExtensionAPI) {
	let state: HookState = {
		projectDir: process.cwd(),
		hooks: [],
	};

	const refreshHooks = (cwd: string) => {
		state = loadHooks(cwd);
		const postCount = state.hooks.filter((hook) => hook.eventName === "PostToolUse").length;
		const failureCount = state.hooks.filter(
			(hook) => hook.eventName === "PostToolUseFailure",
		).length;
		logHook(
			`loaded hooks projectDir=${state.projectDir} postToolUse=${postCount} postToolUseFailure=${failureCount}`,
		);
	};

	pi.on("session_start", (_event, ctx) => {
		refreshHooks(ctx.cwd);
	});

	pi.on("session_switch", (_event, ctx) => {
		refreshHooks(ctx.cwd);
	});

	pi.on("tool_result", async (event, ctx) => {
		if (state.hooks.length === 0) {
			return;
		}

		const eventName = hookEventNameForResult(event);
		const matchingHooks = state.hooks.filter(
			(hook) => hook.eventName === eventName && matchesHook(hook, event.toolName),
		);
		if (matchingHooks.length === 0) {
			return;
		}

		const payload = buildHookPayload(event, eventName, ctx, state.projectDir);
		const executedCommands = new Set<string>();

		for (const hook of matchingHooks) {
			if (executedCommands.has(hook.command)) {
				logHook(
					`deduped event=${eventName} tool=${event.toolName} source=${hook.source} command=${JSON.stringify(hook.command)}`,
				);
				continue;
			}
			executedCommands.add(hook.command);

			logHook(
				`run event=${eventName} tool=${event.toolName} matcher=${hook.matcherText ?? "*"} source=${hook.source} command=${JSON.stringify(hook.command)}`,
			);

			const result = await runCommandHook(hook.command, state.projectDir, payload);
			logHook(
				`done event=${eventName} tool=${event.toolName} code=${result.code} durationMs=${result.elapsedMs} timedOut=${result.timedOut}`,
			);

			if (result.code !== 0) {
				const matcherLabel = hook.matcherText ?? "*";
				const errorLine =
					result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
				ctx.ui.notify(
					`Hook failed (${matcherLabel}) from ${hook.source}: ${errorLine}`,
					"warning",
				);
			}
		}
	});
}
