import type { PluginInput, Hooks } from '@opencode-ai/plugin';
import { tool } from '@opencode-ai/plugin';
import { StructuredError } from '@agentuity/core';
import { existsSync } from 'node:fs';
import { homedir, platform } from 'node:os';
import { join } from 'node:path';
import { z } from 'zod';
import type { AgentConfig, CommandDefinition } from '../types';
import { loadAllSkills, type LoadedSkill } from '../skills';
import { agents } from '../agents';
import { loadCoderConfig, getDefaultConfig, mergeConfig, validateAndWarnConfigs } from '../config';
import { createSessionHooks } from './hooks/session';
import { createToolHooks, getCoderProfile } from './hooks/tools';
import { createKeywordHooks } from './hooks/keyword';
import { createParamsHooks } from './hooks/params';
import { createCadenceHooks } from './hooks/cadence';
import { createSessionMemoryHooks } from './hooks/session-memory';
import { createCompletionHooks } from './hooks/completion';
import type { AgentRole } from '../types';
import type { SessionTreeNode } from '../sqlite';
import { OpenCodeDBReader } from '../sqlite';
import { TmuxSessionManager } from '../tmux';
import { checkAuth } from '../services/auth';

const sessionInputSchema = z.object({
	sessionID: z.string().optional(),
});

// ─────────────────────────────────────────────────────────────────────────────
// Memory Share Tool Errors
// ─────────────────────────────────────────────────────────────────────────────

const MemoryShareAuthError = StructuredError(
	'MemoryShareAuthError',
	'Authentication required to share memory content'
)<{ reason: string }>();

const MemoryShareCLIError = StructuredError(
	'MemoryShareCLIError',
	'CLI command failed to create stream'
)<{ exitCode: number; stderr: string }>();

const MemoryShareError = StructuredError(
	'MemoryShareError',
	'Failed to create public memory share'
)<{ reason: string }>();

const OpenCodeDashboardUnavailableError = StructuredError(
	'OpenCodeDashboardUnavailableError',
	'OpenCode SQLite database is not available. Requires OpenCode v1.2.0+ with SQLite storage.'
);

// Sandbox environment detection
const SANDBOX_ID = process.env.AGENTUITY_SANDBOX_ID;
const IN_SANDBOX = !!SANDBOX_ID;

// Sandbox context injected into Lead, Builder, and Architect prompts
const SANDBOX_CONTEXT = IN_SANDBOX
	? `
## Sandbox Environment

You are running inside an Agentuity Sandbox (ID: ${SANDBOX_ID}).

**Permissions:** All file operations are allowed without prompts.

**File Locations:**
- Working directory: \`/home/agentuity\`
- Temp files: \`/home/agentuity/tmp/\` (preferred over \`/tmp/\`)
- Artifacts: \`/home/agentuity/.agentuity/\`

**Tips:**
- No permission prompts - you can read/write freely
- Sandbox is isolated - safe to experiment
- Use \`/home/agentuity/\` paths for all file operations
`
	: '';

// Agents that should receive sandbox context in their prompts
const SANDBOX_AWARE_AGENTS: AgentRole[] = ['lead', 'builder', 'architect'];

export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
	ctx.client.app.log({
		body: {
			service: 'agentuity-coder',
			level: 'info',
			message: 'Agentuity Coder plugin initializing',
		},
	});

	const userConfig = await loadCoderConfig();
	const coderConfig = mergeConfig(getDefaultConfig(), userConfig);
	const resolvedDbPath = resolveOpenCodeDBPath();
	const dbReader = new OpenCodeDBReader(resolvedDbPath ? { dbPath: resolvedDbPath } : undefined);

	// Shared Map: chat.params stores the user's message text per session,
	// chat.message reads it for trigger detection (avoids scanning model output).
	const lastUserMessages = new Map<string, string>();

	const sessionHooks = createSessionHooks(ctx, coderConfig);
	const toolHooks = createToolHooks(ctx, coderConfig);
	const keywordHooks = createKeywordHooks(ctx, coderConfig);
	const paramsHooks = createParamsHooks(ctx, coderConfig);
	const completionHooks = createCompletionHooks(ctx, coderConfig);
	const tmuxManager = coderConfig.tmux?.enabled
		? new TmuxSessionManager(ctx, coderConfig.tmux, {
				onLog: (message) =>
					ctx.client.app.log({
						body: {
							service: 'agentuity-coder',
							level: 'info',
							message,
						},
					}),
			})
		: undefined;

	// Create hooks for session routing and compaction behavior
	const cadenceHooks = createCadenceHooks(ctx, coderConfig, dbReader, lastUserMessages);

	// Session memory hooks handle checkpointing and compaction for non-Cadence sessions
	// Orchestration (deciding which module handles which session) happens below in the hooks
	const sessionMemoryHooks = createSessionMemoryHooks(ctx, coderConfig, dbReader);

	const configHandler = createConfigHandler(coderConfig);

	// Create plugin tools using the @opencode-ai/plugin tool helper
	const tools = createTools(dbReader);

	// Create a logger for shutdown handler
	const shutdownLogger = (message: string) =>
		ctx.client.app.log({
			body: {
				service: 'agentuity-coder',
				level: 'info',
				message: `[shutdown] ${message}`,
			},
		});

	registerShutdownHandler(tmuxManager, shutdownLogger, dbReader);

	// Show startup toast (fire and forget, don't block)
	try {
		ctx.client.tui.showToast({
			body: { message: '🚀 Agentuity Coder ready', variant: 'success' },
		});
	} catch {
		// Toast may not be available
	}

	return {
		...(tools ? { tool: tools } : {}),
		config: configHandler,
		'chat.message': async (input: unknown, output: unknown) => {
			completionHooks.onMessage(input);
			await keywordHooks.onMessage(input, output);
			await sessionHooks.onMessage(input, output);
			await cadenceHooks.onMessage(input, output);
		},
		'chat.params': async (input: unknown, output: unknown) => {
			completionHooks.onParams(input);
			await paramsHooks.onParams(input, output);
		},
		'tool.execute.before': toolHooks.before,
		'tool.execute.after': toolHooks.after,
		'shell.env': async (_input: unknown, output: unknown) => {
			if (typeof output !== 'object' || output === null) {
				return;
			}
			const profile = getCoderProfile();
			const out = output as { env: Record<string, string> };
			out.env ??= {} as Record<string, string>;
			out.env.AGENTUITY_PROFILE = profile;
			out.env.AGENTUITY_AGENT_MODE = 'opencode';
			if (resolvedDbPath) {
				out.env.OPENCODE_DB_PATH = resolvedDbPath;
			}
			const sessionId = process.env.AGENTUITY_OPENCODE_SESSION;
			if (sessionId) {
				out.env.AGENTUITY_OPENCODE_SESSION = sessionId;
			}
		},
		'command.execute.before': async (input: unknown, _output: unknown) => {
			const result = sessionInputSchema.safeParse(input);
			if (result.success && result.data.sessionID) {
				process.env.AGENTUITY_OPENCODE_SESSION = result.data.sessionID;
			}
		},
		event: async (input) => {
			// Orchestrate: route to appropriate module based on session type
			const sessionId = extractSessionIdFromEvent(input);
			if (sessionId) {
				// Try lazy restore from KV if not in memory (survives plugin restarts)
				if (!cadenceHooks.isActiveCadenceSession(sessionId)) {
					await cadenceHooks.tryRestoreFromKV(sessionId);
				}

				if (cadenceHooks.isActiveCadenceSession(sessionId)) {
					await cadenceHooks.onEvent(input);
				} else {
					// Non-Cadence sessions - handle session.compacted for checkpointing
					await sessionMemoryHooks.onEvent(
						input as { event: { type: string; properties?: Record<string, unknown> } }
					);
				}
			}
		},
		'experimental.session.compacting': async (input, output) => {
			// Try lazy restore from KV if not in memory (survives plugin restarts)
			if (!cadenceHooks.isActiveCadenceSession(input.sessionID)) {
				await cadenceHooks.tryRestoreFromKV(input.sessionID);
			}

			// Orchestrate: route to appropriate module based on session type
			if (cadenceHooks.isActiveCadenceSession(input.sessionID)) {
				await cadenceHooks.onCompacting(input, output);
			} else {
				await sessionMemoryHooks.onCompacting(input, output);
			}
		},
	};
}

function createConfigHandler(
	coderConfig: ReturnType<typeof getDefaultConfig>
): (config: Record<string, unknown>) => Promise<void> {
	return async (config: Record<string, unknown>) => {
		const agentConfigs = createAgentConfigs(coderConfig);
		const commands = createCommands();
		const loadedSkills = await loadAllSkills(coderConfig.skills);
		const skillCommands = createSkillCommands(loadedSkills);

		// Merge agent configs: our defaults first, then user's opencode.json overrides on top
		// This allows users to customize any agent via their opencode.json
		const userAgentConfigs = config.agent as Record<string, AgentConfig> | undefined;
		const mergedAgents: Record<string, AgentConfig> = { ...agentConfigs };

		// Shallow merge user overrides on top of our defaults (nested objects like tools are replaced, not merged)
		if (userAgentConfigs) {
			for (const [name, userConfig] of Object.entries(userAgentConfigs)) {
				if (mergedAgents[name]) {
					// Merge user config on top of our default
					mergedAgents[name] = {
						...mergedAgents[name],
						...userConfig,
					};
				} else {
					// User defined a new agent not in our defaults
					mergedAgents[name] = userConfig;
				}
			}
		}

		config.agent = mergedAgents;

		// Validate merged configs and warn about mismatches
		validateAndWarnConfigs(mergedAgents);

		// Permission configuration for external directories
		// Memory agent and other operations may need to write temp files for CLI piping
		if (IN_SANDBOX) {
			// In sandbox, allow all permissions without prompts
			config.permission = {
				'*': 'allow',
				external_directory: {
					'/home/agentuity/**': 'allow',
					'*': 'allow',
				},
			};
		} else {
			// For non-sandbox environments, auto-allow temp directory writes
			// This prevents blocking prompts when Memory agent writes large JSON for CLI piping
			const existingPermissions = (config.permission as Record<string, unknown>) ?? {};
			const existingExternalDir =
				(existingPermissions.external_directory as Record<string, string>) ?? {};

			// Normalize TMPDIR: strip trailing slashes, then append /**
			const tmpdir = process.env.TMPDIR?.replace(/\/+$/, '');
			const tmpdirPattern = tmpdir ? `${tmpdir}/**` : null;

			config.permission = {
				...existingPermissions,
				external_directory: {
					...existingExternalDir,
					'/tmp/**': 'allow',
					// Also allow OS-specific temp directories
					...(tmpdirPattern ? { [tmpdirPattern]: 'allow' } : {}),
				},
			};
		}

		// Compaction config: increase reserved token buffer to accommodate our enriched
		// compaction prompts (planning state, image descriptions, tool summaries, diagnostics).
		// Default OpenCode reserved buffer is too small for the context we inject.
		const existingCompaction = (config.compaction ?? {}) as Record<string, unknown>;
		const existingReserved = existingCompaction.reserved;
		config.compaction = {
			...existingCompaction,
			reserved: typeof existingReserved === 'number' ? existingReserved : 40_000,
		};

		config.command = {
			...(config.command as Record<string, CommandDefinition> | undefined),
			...commands,
			...skillCommands,
		};
	};
}

function createAgentConfigs(
	_config: ReturnType<typeof getDefaultConfig>
): Record<string, AgentConfig> {
	const result: Record<string, AgentConfig> = {};

	for (const agent of Object.values(agents)) {
		// Convert tools.exclude to Open Code format (tool: false)
		const tools: Record<string, boolean> = {};
		if (agent.tools?.exclude) {
			for (const tool of agent.tools.exclude) {
				tools[tool] = false;
			}
		}

		// Inject sandbox context into specific agents when running in sandbox
		const shouldInjectSandbox =
			IN_SANDBOX && SANDBOX_AWARE_AGENTS.includes(agent.role as AgentRole);
		const prompt = shouldInjectSandbox
			? `${agent.systemPrompt}\n${SANDBOX_CONTEXT}`
			: agent.systemPrompt;

		// Use agent defaults directly - user overrides happen in createConfigHandler
		result[agent.displayName] = {
			description: agent.description,
			model: agent.defaultModel,
			prompt,
			mode: agent.mode ?? 'subagent',
			...(Object.keys(tools).length > 0 ? { tools } : {}),
			...(agent.variant ? { variant: agent.variant } : {}),
			...(agent.temperature !== undefined ? { temperature: agent.temperature } : {}),
			...(agent.maxSteps !== undefined ? { maxSteps: agent.maxSteps } : {}),
			...(agent.reasoningEffort ? { reasoningEffort: agent.reasoningEffort } : {}),
			...(agent.thinking ? { thinking: agent.thinking } : {}),
			...(agent.hidden ? { hidden: agent.hidden } : {}),
			...(agent.fallbackModels?.length ? { fallbackModels: agent.fallbackModels } : {}),
		};
	}

	return result;
}

function createCommands(): Record<string, CommandDefinition> {
	return {
		'agentuity-coder': {
			name: 'agentuity-coder',
			description:
				'Run a task with the Agentuity Coder agent team (use @Agentuity Coder Lead, @Agentuity Coder Scout, etc.)',
			template: `<coder-mode>
You are the Agentuity Coder Lead agent orchestrating the Agentuity Coder team.

## Your Team (use @mentions to invoke)
- **@Agentuity Coder Scout**: Explore codebase, find patterns, research docs (read-only)
- **@Agentuity Coder Builder**: Implement features, write code, run tests
- **@Agentuity Coder Architect**: Complex autonomous tasks, Cadence mode (GPT Codex)
- **@Agentuity Coder Reviewer**: Review changes, catch issues, apply fixes
- **@Agentuity Coder Memory**: Store context, remember decisions, extract conclusions
- **@Agentuity Coder Expert**: Agentuity CLI, SDK, Services, and Documentation specialist
- **@Agentuity Coder Runner**: Run lint/build/test commands, returns structured results
- **@Agentuity Coder Product**: Clarify requirements, validate features, track progress

## Task
$ARGUMENTS

## Guidelines
1. Use @Agentuity Coder Scout first to understand context
2. Use @Agentuity Coder Product to clarify requirements if unclear
3. Delegate implementation to @Agentuity Coder Builder (or Architect for complex work)
4. Delegate lint/build/test commands to @Agentuity Coder Runner for structured results
5. Have @Agentuity Coder Reviewer check the work
6. Use @Agentuity Coder Expert for Agentuity CLI, SDK, Services, and Documentation questions
7. Only use cloud services when genuinely helpful
8. **When done, tell @Agentuity Coder Memory to memorialize the session**
</coder-mode>`,
			agent: 'Agentuity Coder Lead',
			argumentHint: '"task description"',
		},
		'agentuity-memory-save': {
			name: 'agentuity-memory-save',
			description: 'Save the current session to memory for future recall',
			template: `Memorialize this session. Summarize what was accomplished in this conversation:
- Problem/task that was addressed
- Key decisions and their rationale  
- Corrections/mistakes (user corrected agent or agent corrected user)
- Patterns and approaches used
- Solutions implemented
- Files and folders referenced
- Open questions or follow-ups

Save to vector storage using the agentuity-opencode-sessions namespace. Store any corrections prominently in agentuity-opencode-memory KV.

$ARGUMENTS`,
			agent: 'Agentuity Coder Memory',
			argumentHint: '(optional additional context)',
		},

		'agentuity-memory-share': {
			name: 'agentuity-memory-share',
			description: '🔗 Share memory content publicly with a shareable URL',
			template: `User wants to share content publicly.

**You have current session context. Memory does not (unless given a session ID).**

- Current session → Handle directly: compile content, call \`agentuity_memory_share\`
- Stored content (specific ID, past work) → Delegate to Memory
- Long Cadence cycle? → Ask Memory for past compactions to include

User's request: $ARGUMENTS`,
			agent: 'Agentuity Coder Lead',
			argumentHint:
				'"share a summary of this session" or "share the auth decisions with 1 hour TTL"',
		},

		// ─────────────────────────────────────────────────────────────────────
		// Agentuity Cloud Service Commands
		// ─────────────────────────────────────────────────────────────────────

		'agentuity-cloud': {
			name: 'agentuity-cloud',
			description: '☁️ Agentuity cloud services (KV, Storage, Vector, Sandbox, DB, SSH, etc.)',
			template: `You are the Agentuity Coder Expert helping with Agentuity cloud services.

Use the \`agentuity\` CLI to execute the user's request.

## Available Services
| Service | CLI | Purpose |
|---------|-----|---------|
| KV | \`agentuity cloud kv\` | Key-value storage (namespaces, keys) |
| Storage | \`agentuity cloud storage\` | Object/file storage (buckets) |
| Vector | \`agentuity cloud vector\` | Embeddings & semantic search |
| Sandbox | \`agentuity cloud sandbox\` | Isolated execution environments |
| Database | \`agentuity cloud db\` | Postgres databases |
| SSH | \`agentuity cloud ssh\` | SSH into deployments/sandboxes |
| Deployments | \`agentuity cloud deployment\` | Manage deployments |
| Agents | \`agentuity cloud agent\` | Cloud agent management |
| Sessions | \`agentuity cloud session\` | Agent session data |
| Threads | \`agentuity cloud thread\` | Conversation threads |

## Guidelines
1. First check auth: \`agentuity auth whoami\`
2. Prefer \`--json\` for programmatic output
3. List/inspect before creating new resources
4. Explain what commands you're running

## User Request
$ARGUMENTS`,
			agent: 'Agentuity Coder Expert',
			subtask: true,
			argumentHint: '"list kv namespaces" or "upload file.txt to storage"',
		},

		'agentuity-sandbox': {
			name: 'agentuity-sandbox',
			description: '🏖️ Agentuity sandboxes (isolated execution environments)',
			template: `You are the Agentuity Coder Expert helping with Agentuity sandboxes.

Use the \`agentuity cloud sandbox\` CLI commands to help the user.

## Common Commands
\`\`\`bash
agentuity cloud sandbox runtime list --json                            # List available runtimes (bun:1, python:3.14, etc.)
agentuity cloud sandbox run [--memory 1Gi] [--cpu 1000m] \\
  [--runtime <name>] [--runtimeId <id>] \\
  [--name <name>] [--description <text>] \\
  -- <command>                                                         # One-shot execution
agentuity cloud sandbox create --json [--memory 1Gi] [--cpu 1000m] \\
  [--network] [--runtime <name>] [--runtimeId <id>] \\
  [--name <name>] [--description <text>]                              # Create persistent sandbox
agentuity cloud sandbox list --json                                   # List sandboxes (includes telemetry)
agentuity cloud sandbox exec <id> -- <command>                        # Run in existing sandbox
agentuity cloud sandbox files <id> [path] --json                      # List files
agentuity cloud sandbox cp ./local <id>:/home/agentuity               # Copy files to sandbox
agentuity cloud sandbox delete <id> --json                            # Delete sandbox
agentuity cloud sandbox snapshot create <id> \\
  [--name <name>] [--description <text>] [--tag <tag>]                # Save sandbox state
\`\`\`

## Guidelines
1. First check auth: \`agentuity auth whoami\`
2. Use \`--json\` for programmatic output
3. Explain what commands you're running
4. Default working directory inside sandboxes: \`/home/agentuity\`
5. Use \`runtime list\` to find runtimes, then pass \`--runtime\` or \`--runtimeId\` on \`run\`/\`create\`
6. Use \`--name\` and \`--description\` for better tracking
7. Snapshot \`--tag\` defaults to \`latest\`, max 128 chars, must match \`^[a-zA-Z0-9][a-zA-Z0-9._-]*$\`
8. Telemetry fields from \`list\`/\`get\`: \`cpuTimeMs\`, \`memoryByteSec\`, \`networkEgressBytes\`, \`networkEnabled\`, \`mode\`

## User Request
$ARGUMENTS`,
			agent: 'Agentuity Coder Expert',
			subtask: true,
			argumentHint: '"run bun test" or "create a sandbox with 2Gi memory"',
		},

		// ─────────────────────────────────────────────────────────────────────
		// Agentuity Cadence Commands (Long-Running Tasks)
		// ─────────────────────────────────────────────────────────────────────

		'agentuity-cadence': {
			name: 'agentuity-cadence',
			description: '🔄 Start a long-running Cadence loop (autonomous task completion)',
			template: `[CADENCE MODE]

You are the Agentuity Coder Lead in **Cadence mode** — a long-running autonomous loop.

## Your Team (use @mentions to invoke)
- **@Agentuity Coder Scout**: Explore codebase, find patterns, research docs (read-only)
- **@Agentuity Coder Architect**: Complex autonomous implementation (GPT Codex with high reasoning) — **USE THIS FOR CADENCE**
- **@Agentuity Coder Builder**: Quick fixes, simple changes (for minor iterations only)
- **@Agentuity Coder Reviewer**: Review changes, catch issues, apply fixes
- **@Agentuity Coder Memory**: Store context, remember decisions, checkpoints, extract conclusions
- **@Agentuity Coder Expert**: Agentuity CLI, SDK, Services, and Documentation specialist
- **@Agentuity Coder Runner**: Run lint/build/test commands, returns structured results
- **@Agentuity Coder Product**: Clarify requirements, validate features, track progress, Cadence briefings

## Task
$ARGUMENTS

## Cadence Workflow

1. **FIRST: Establish PRD with Product** (REQUIRED):
   - Ask @Agentuity Coder Product to establish/validate the PRD for this task
   - Product will check for existing PRD or create one
   - This defines "what" we're building and success criteria

2. **Initialize loop state**:
   - Generate loop ID (format: \`lp_short_name_01\`)
   - Store in KV: \`agentuity cloud kv set agentuity-opencode-tasks "loop:{loopId}:state" '{...}'\`
   - Link session planning to PRD via \`prdKey\`

3. **Each iteration**:
   - Ask @Agentuity Coder Memory for relevant context
   - Use @Agentuity Coder Scout to understand what's needed
   - For complex planning, use extended thinking (ultrathink) — ground in PRD requirements
   - Delegate implementation to **@Agentuity Coder Architect** (preferred for Cadence)
   - Have @Agentuity Coder Reviewer verify the work
   - Tell @Agentuity Coder Memory to store checkpoint

4. **When truly complete**, output:
\`\`\`
<promise>DONE</promise>
\`\`\`

5. **Finalize**:
   - Tell @Agentuity Coder Product to update the PRD with completed work
   - Tell @Agentuity Coder Memory to memorialize the session

## Guidelines
- **Product first** — Always establish PRD before starting work
- **Use Architect for implementation** — Architect has GPT Codex with maximum reasoning, ideal for autonomous work
- Use regular Builder only for trivial fixes within an iteration
- Ask Memory for context at each iteration start
- Store checkpoints at each iteration end
- If stuck on architecture, use extended thinking (ultrathink) for deep planning
- Use @Agentuity Coder Expert for sandbox/cloud operations
- Respect max iterations (50 default)

## Lead-of-Leads (Parallel Work)
If the task has **independent workstreams** that can run in parallel (e.g., "build auth, payments, and notifications"):
1. Ask @Agentuity Coder Product to create PRD with workstreams
2. Spawn child Leads using the \`task\` tool (issue multiple task calls in a single response for parallel work)
3. Each child Lead claims a workstream, works autonomously, marks done when complete
4. Monitor progress via PRD workstream status
5. Do integration work when all children complete

**Don't use Lead-of-Leads for:** small tasks, sequential work, or work requiring tight coordination.`,
			agent: 'Agentuity Coder Lead',
			argumentHint: 'build the new auth feature with tests',
		},
	};
}

function createSkillCommands(skills: LoadedSkill[]): Record<string, CommandDefinition> {
	const commands: Record<string, CommandDefinition> = {};

	for (const skill of skills) {
		const baseDir = normalizeBaseDir(skill.resolvedPath);
		commands[skill.name] = {
			name: skill.name,
			description: skill.metadata.description,
			template: `<skill-instruction>
Base directory for this skill: ${baseDir}/
File references (@path) in this skill are relative to this directory.

${skill.content}
</skill-instruction>

<user-request>
$ARGUMENTS
</user-request>`,
			...(skill.metadata.agent ? { agent: skill.metadata.agent } : {}),
			...(skill.metadata.model ? { model: skill.metadata.model } : {}),
			...(skill.metadata['argument-hint']
				? { argumentHint: skill.metadata['argument-hint'] }
				: {}),
			...(skill.metadata.subtask ? { subtask: true } : {}),
		};
	}

	return commands;
}

function normalizeBaseDir(path: string): string {
	return path.replace(/[\\/]+$/, '');
}

function createTools(dbReader?: OpenCodeDBReader): Hooks['tool'] {
	// Use the schema from @opencode-ai/plugin's tool helper to avoid Zod version mismatches
	const s = tool.schema;

	const sessionDashboard = tool({
		description:
			'Inspect a parent session dashboard from the local OpenCode SQLite database. Useful for Lead-of-Leads monitoring and nested session visibility.',
		args: {
			session_id: s.string().describe('Parent session ID to inspect'),
		},
		async execute(args) {
			if (!dbReader || !dbReader.isAvailable()) {
				const err = new OpenCodeDashboardUnavailableError();
				return JSON.stringify({
					success: false,
					error: err._tag,
					message: err.message,
				});
			}

			const dashboard = dbReader.getSessionDashboard(args.session_id);
			const builtTree = buildDashboardTree(dbReader, dashboard.sessions);
			return JSON.stringify({
				success: true,
				sessionId: args.session_id,
				totalCost: dashboard.totalCost,
				summary: computeHealthSummary(builtTree),
				sessions: builtTree,
			});
		},
	});

	const memoryShare = tool({
		description: `Share memory content publicly via Agentuity Cloud Streams.

Creates a public URL that can be shared with anyone - no authentication required to access.
The content is stored in Agentuity's durable stream storage with optional TTL.

Use this when:
- User wants to share context with another agent/session
- User wants to export a summary, compaction, or session for external use
- User explicitly asks to "share" or "make public" some memory content

Returns the public URL that can be copied and used anywhere.`,
		args: {
			content: s.string().describe('The content to share publicly'),
			namespace: s
				.string()
				.optional()
				.describe('Stream namespace (default: agentuity-opencode-shares)'),
			ttl_seconds: s
				.number()
				.optional()
				.describe('TTL in seconds (60-31536000, or omit for 30-day default)'),
			content_type: s.string().optional().describe('Content type (default: text/markdown)'),
			metadata: s
				.record(s.string(), s.string())
				.optional()
				.describe('Optional metadata key-value pairs'),
			compress: s.boolean().optional().describe('Enable gzip compression'),
			region: s.string().optional().describe('Cloud region (use, usc, usw). Default: usc'),
		},
		async execute(args, context) {
			// Get the profile first - this ensures checkAuth() and CLI use the same profile
			const profile = getCoderProfile();
			const originalProfile = process.env.AGENTUITY_PROFILE;
			const sessionId = context.sessionID;

			try {
				// Set profile before auth check so checkAuth reads the correct config
				process.env.AGENTUITY_PROFILE = profile;

				// Check auth first
				const authResult = await checkAuth();
				if (!authResult.ok) {
					const err = new MemoryShareAuthError({ reason: authResult.error });
					return JSON.stringify({
						success: false,
						error: err.message,
						errorTag: err._tag,
						details: { reason: authResult.error },
					});
				}

				// Build CLI command
				const namespace = args.namespace ?? 'agentuity-opencode-shares';
				const contentType = args.content_type ?? 'text/markdown';

				const cliArgs = ['agentuity', '--json', 'cloud', 'stream', 'create', namespace, '-'];
				cliArgs.push('--content-type', contentType);
				cliArgs.push('--region', args.region ?? 'usc');

				if (args.ttl_seconds !== undefined) {
					cliArgs.push('--ttl', String(args.ttl_seconds));
				}

				if (args.compress) {
					cliArgs.push('--compress');
				}

				if (args.metadata && Object.keys(args.metadata).length > 0) {
					const metadataStr = Object.entries(args.metadata)
						.map(([k, v]) => `${k}=${v}`)
						.join(',');
					cliArgs.push('--metadata', metadataStr);
				}
				const proc = Bun.spawn(cliArgs, {
					stdin: 'pipe',
					stdout: 'pipe',
					stderr: 'pipe',
					env: {
						...process.env,
						AGENTUITY_PROFILE: profile,
						AGENTUITY_AGENT_MODE: 'opencode',
						...(sessionId ? { AGENTUITY_OPENCODE_SESSION: sessionId } : {}),
					},
				});

				// Write content to stdin (Bun's FileSink API)
				proc.stdin.write(new TextEncoder().encode(args.content));
				proc.stdin.end();

				const [stdout, stderr, exitCode] = await Promise.all([
					new Response(proc.stdout).text(),
					new Response(proc.stderr).text(),
					proc.exited,
				]);

				if (exitCode !== 0) {
					const err = new MemoryShareCLIError({
						exitCode,
						stderr: stderr || `CLI exited with code ${exitCode}`,
					});
					return JSON.stringify({
						success: false,
						error: err.message,
						errorTag: err._tag,
						details: { exitCode, stderr },
					});
				}

				// Parse JSON response from CLI
				const result = JSON.parse(stdout);

				return JSON.stringify({
					success: true,
					url: result.url,
					id: result.id,
					namespace: result.namespace,
					sizeBytes: result.sizeBytes,
					expiresAt: result.expiresAt,
				});
			} catch (error) {
				const reason = error instanceof Error ? error.message : 'Failed to create stream';
				const err = new MemoryShareError({ reason });
				return JSON.stringify({
					success: false,
					error: err.message,
					errorTag: err._tag,
					details: { reason },
				});
			} finally {
				// Restore original profile
				if (originalProfile !== undefined) {
					process.env.AGENTUITY_PROFILE = originalProfile;
				} else {
					delete process.env.AGENTUITY_PROFILE;
				}
			}
		},
	});

	return {
		agentuity_session_dashboard: sessionDashboard,
		agentuity_memory_share: memoryShare,
	};
}

// ─────────────────────────────────────────────────────────────────────────────
// Helper Functions
// ─────────────────────────────────────────────────────────────────────────────

function extractSessionIdFromEvent(input: unknown): string | undefined {
	if (typeof input !== 'object' || input === null) return undefined;

	const inp = input as { event?: { properties?: Record<string, unknown> } };
	if (!inp.event?.properties) return undefined;

	return (
		(inp.event.properties.sessionId as string | undefined) ??
		(inp.event.properties.sessionID as string | undefined)
	);
}

function parseDisplayTitle(rawTitle: string): string {
	try {
		const parsed = JSON.parse(rawTitle);
		if (typeof parsed === 'object' && parsed !== null) {
			if (typeof parsed.description === 'string') return parsed.description;
			if (typeof parsed.title === 'string') return parsed.title;
			if (typeof parsed.name === 'string') return parsed.name;
		}
		// Parsed but no useful field — return as-is
		return rawTitle;
	} catch {
		// Not JSON, return as-is
		return rawTitle;
	}
}

function getCurrentActivity(reader: OpenCodeDBReader, sessionId: string): string | null {
	// First check active tools
	const activeTools = reader.getActiveToolCalls(sessionId);
	const firstTool = activeTools[0];
	if (firstTool) {
		return `Running ${firstTool.tool}`;
	}

	// Fall back to latest message
	const latestMsg = reader.getLatestMessage(sessionId);
	if (!latestMsg) return null;

	if (latestMsg.error) return `Error: ${latestMsg.error.substring(0, 100)}`;

	// Try to get latest text part for a snippet
	const textParts = reader.getTextParts(sessionId, { limit: 1 });
	const firstPart = textParts[0];
	if (firstPart) {
		const text = firstPart.text.trim();
		if (text.length > 80) return text.substring(0, 77) + '...';
		return text;
	}

	return null;
}

type DashboardNode = {
	session: {
		id: string;
		title: string;
		displayTitle: string;
		parentId?: string | null;
		timeUpdated: number;
		timeUpdatedISO: string;
	};
	status: string;
	lastActivity: number;
	lastActivityISO: string;
	currentActivity: string | null;
	messageCount: number;
	activeToolCount: number;
	activeTools: Array<{ tool: string; status: string }>;
	todoSummary: { total: number; pending: number; completed: number };
	costSummary?: SessionTreeNode['costSummary'];
	children: DashboardNode[];
};

function buildDashboardTree(
	reader: OpenCodeDBReader,
	sessions: SessionTreeNode[]
): DashboardNode[] {
	return sessions.map((node) => {
		const status = reader.getSessionStatus(node.session.id);
		const activeTools = reader.getActiveToolCalls(node.session.id);
		return {
			session: {
				id: node.session.id,
				title: node.session.title,
				displayTitle: parseDisplayTitle(node.session.title),
				parentId: node.session.parentId,
				timeUpdated: node.session.timeUpdated,
				timeUpdatedISO: new Date(node.session.timeUpdated).toISOString(),
			},
			status: status.status,
			lastActivity: status.lastActivity,
			lastActivityISO: new Date(status.lastActivity).toISOString(),
			currentActivity: getCurrentActivity(reader, node.session.id),
			messageCount: node.messageCount,
			activeToolCount: node.activeToolCount,
			activeTools: activeTools.map((t) => ({ tool: t.tool, status: t.status })),
			todoSummary: node.todoSummary ?? { total: 0, pending: 0, completed: 0 },
			costSummary: node.costSummary,
			children: buildDashboardTree(reader, node.children),
		};
	});
}

type HealthSummary = {
	active: number;
	idle: number;
	error: number;
	archived: number;
	compacting: number;
	total: number;
};

function computeHealthSummary(nodes: DashboardNode[]): HealthSummary {
	const summary: HealthSummary = {
		active: 0,
		idle: 0,
		error: 0,
		archived: 0,
		compacting: 0,
		total: 0,
	};
	function walk(nodeList: DashboardNode[]): void {
		for (const node of nodeList) {
			summary.total++;
			const status = node.status as keyof Omit<HealthSummary, 'total'>;
			if (status in summary) {
				summary[status]++;
			}
			if (node.children.length > 0) walk(node.children);
		}
	}
	walk(nodes);
	return summary;
}

function resolveOpenCodeDBPath(): string | null {
	const envPath = process.env.OPENCODE_DB_PATH;
	if (envPath) {
		if (isMemoryPath(envPath)) return envPath;
		if (existsSync(envPath)) return envPath;
	}

	const home = homedir();
	const candidates: string[] = [];
	const currentPlatform = platform();

	if (currentPlatform === 'darwin') {
		candidates.push(join(home, 'Library', 'Application Support', 'opencode', 'opencode.db'));
	}

	if (currentPlatform === 'win32') {
		const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
		const localAppData = process.env.LOCALAPPDATA ?? join(home, 'AppData', 'Local');
		candidates.push(join(appData, 'opencode', 'opencode.db'));
		candidates.push(join(localAppData, 'opencode', 'opencode.db'));
	}

	// Linux default
	candidates.push(join(home, '.local', 'share', 'opencode', 'opencode.db'));

	for (const candidate of candidates) {
		if (existsSync(candidate)) {
			return candidate;
		}
	}

	return null;
}

function isMemoryPath(path: string): boolean {
	return path === ':memory:' || path.includes('mode=memory');
}

function registerShutdownHandler(
	tmuxManager?: TmuxSessionManager,
	logger?: (msg: string) => void,
	dbReader?: OpenCodeDBReader
): void {
	if (typeof process === 'undefined') {
		logger?.('[shutdown] process is undefined, cannot register handlers');
		return;
	}

	const log = logger ?? (() => {});
	let shutdownCalled = false;

	log(
		`Registering shutdown handlers (PID: ${process.pid}, tmuxManager: ${tmuxManager ? 'yes' : 'no'})`
	);
	log(`Current tracked sessions in tmuxManager: ${tmuxManager ? 'checking...' : 'N/A'}`);

	const shutdown = (signal?: string) => {
		// Prevent multiple shutdown calls
		if (shutdownCalled) {
			log(`Shutdown already in progress, ignoring ${signal ?? 'unknown'} signal`);
			return;
		}
		shutdownCalled = true;

		log(`Shutdown triggered by ${signal ?? 'unknown'} signal`);

		if (tmuxManager) {
			try {
				log('Cleaning up tmux sessions...');
				// Use sync version to ensure cleanup completes before process exits
				tmuxManager.cleanupSync();
				log('Tmux cleanup complete');
			} catch (error) {
				log(`Tmux cleanup error: ${error}`);
			}
		}

		if (dbReader) {
			try {
				log('Closing OpenCode DB reader...');
				dbReader.close();
				log('OpenCode DB reader closed');
			} catch (error) {
				log(`OpenCode DB reader error: ${error}`);
			}
		}

		log('Shutdown complete');
	};

	process.once('beforeExit', () => shutdown('beforeExit'));
	process.once('SIGINT', () => shutdown('SIGINT'));
	process.once('SIGTERM', () => shutdown('SIGTERM'));
	process.once('SIGHUP', () => shutdown('SIGHUP')); // Handle tmux pane close
	process.once('exit', () => shutdown('exit')); // Also handle exit event for extra safety

	log('Shutdown handlers registered for: beforeExit, SIGINT, SIGTERM, SIGHUP, exit');
}
