/**
 * Hooks Manager
 *
 * Manages hook loading and execution from .emdash/hooks.json.
 * Hooks are shell commands triggered on specific agent events.
 */

import { promises as fs } from 'fs';
import path from 'path';
import { execa } from 'execa';
import { z } from 'zod';
import { HookEventType, EventType, type AgentEvent, type EventHandler } from './types.js';
import { createLogger, type Logger } from '../utils/logger.js';

/**
 * Hook configuration schema
 */
export const HookConfigSchema = z.object({
  id: z.string().min(1),
  event: z.nativeEnum(HookEventType),
  command: z.string().min(1),
  enabled: z.boolean().default(true),
});

export type HookConfig = z.infer<typeof HookConfigSchema>;

/**
 * Hooks file schema
 */
export const HooksFileSchema = z.object({
  hooks: z.array(HookConfigSchema),
});

export type HooksFile = z.infer<typeof HooksFileSchema>;

/**
 * Hook event data passed to commands
 */
export interface HookEventData {
  event: string;
  timestamp: string;
  sessionId?: string;

  // Tool fields
  toolName?: string;
  toolArgs?: Record<string, unknown>;
  toolResult?: string;
  toolSuccess?: boolean;
  toolError?: string;

  // Response/Session fields
  responseText?: string;
  goal?: string;
  success?: boolean;

  // Error fields
  errorMessage?: string;
  errorDetails?: string;
}

/**
 * Convert HookEventData to JSON
 */
export function hookEventDataToJSON(data: HookEventData): string {
  return JSON.stringify(data, null, 2);
}

/**
 * Convert HookEventData to environment variables (EMDASH_* prefix)
 */
export function hookEventDataToEnvVars(data: HookEventData): Record<string, string> {
  const env: Record<string, string> = {};

  env.EMDASH_EVENT = data.event;
  env.EMDASH_TIMESTAMP = data.timestamp;

  if (data.sessionId) env.EMDASH_SESSION_ID = data.sessionId;
  if (data.toolName) env.EMDASH_TOOL_NAME = data.toolName;
  if (data.toolArgs) env.EMDASH_TOOL_ARGS = JSON.stringify(data.toolArgs);
  if (data.toolResult) env.EMDASH_TOOL_RESULT = data.toolResult;
  if (data.toolSuccess !== undefined) env.EMDASH_TOOL_SUCCESS = String(data.toolSuccess);
  if (data.toolError) env.EMDASH_TOOL_ERROR = data.toolError;
  if (data.responseText) env.EMDASH_RESPONSE = data.responseText;
  if (data.goal) env.EMDASH_GOAL = data.goal;
  if (data.success !== undefined) env.EMDASH_SUCCESS = String(data.success);
  if (data.errorMessage) env.EMDASH_ERROR = data.errorMessage;
  if (data.errorDetails) env.EMDASH_ERROR_DETAILS = data.errorDetails;

  return env;
}

/**
 * Default hooks config path
 */
const DEFAULT_HOOKS_PATH = '.emdash/hooks.json';

/**
 * Hook Manager
 *
 * Manages hook loading, execution, and persistence.
 */
export class HookManager {
  private repoRoot: string;
  private configPath: string;
  private hooks: HookConfig[] = [];
  private sessionId?: string;
  private logger: Logger;
  private loaded = false;

  constructor(repoRoot?: string, logger?: Logger) {
    this.repoRoot = repoRoot ?? process.cwd();
    this.configPath = path.join(this.repoRoot, DEFAULT_HOOKS_PATH);
    this.logger = logger ?? createLogger({ name: 'hook-manager' });
  }

  /**
   * Set session ID for event data
   */
  setSessionId(sessionId: string | undefined): void {
    this.sessionId = sessionId;
  }

  /**
   * Load hooks from config file
   */
  async loadHooks(): Promise<HookConfig[]> {
    try {
      const content = await fs.readFile(this.configPath, 'utf-8');
      const parsed = JSON.parse(content);
      const validated = HooksFileSchema.parse(parsed);
      this.hooks = validated.hooks;
      this.loaded = true;
      this.logger.debug({ count: this.hooks.length }, 'Loaded hooks');
      return this.hooks;
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
        this.hooks = [];
        this.loaded = true;
        return this.hooks;
      }
      this.logger.error({ error }, 'Failed to load hooks');
      this.hooks = [];
      this.loaded = true;
      return this.hooks;
    }
  }

  /**
   * Ensure hooks are loaded
   */
  private async ensureLoaded(): Promise<void> {
    if (!this.loaded) {
      await this.loadHooks();
    }
  }

  /**
   * Get all hooks
   */
  async getHooks(): Promise<HookConfig[]> {
    await this.ensureLoaded();
    return [...this.hooks];
  }

  /**
   * Get enabled hooks for a specific event type
   */
  async getEnabledHooks(event: HookEventType): Promise<HookConfig[]> {
    await this.ensureLoaded();
    return this.hooks.filter((h) => h.enabled && h.event === event);
  }

  /**
   * Add a new hook
   */
  async addHook(hook: HookConfig): Promise<void> {
    await this.ensureLoaded();

    if (this.hooks.some((h) => h.id === hook.id)) {
      throw new Error(`Hook with id "${hook.id}" already exists`);
    }

    this.hooks.push(hook);
    await this.saveHooks();
    this.logger.info({ id: hook.id, event: hook.event }, 'Added hook');
  }

  /**
   * Remove a hook by ID
   */
  async removeHook(hookId: string): Promise<boolean> {
    await this.ensureLoaded();

    const initialLength = this.hooks.length;
    this.hooks = this.hooks.filter((h) => h.id !== hookId);

    if (this.hooks.length === initialLength) {
      return false;
    }

    await this.saveHooks();
    this.logger.info({ id: hookId }, 'Removed hook');
    return true;
  }

  /**
   * Toggle a hook's enabled status
   */
  async toggleHook(hookId: string): Promise<boolean | null> {
    await this.ensureLoaded();

    const hook = this.hooks.find((h) => h.id === hookId);
    if (!hook) {
      return null;
    }

    hook.enabled = !hook.enabled;
    await this.saveHooks();
    this.logger.info({ id: hookId, enabled: hook.enabled }, 'Toggled hook');
    return hook.enabled;
  }

  /**
   * Save hooks to config file
   */
  private async saveHooks(): Promise<void> {
    const dir = path.dirname(this.configPath);
    await fs.mkdir(dir, { recursive: true });

    const content = JSON.stringify({ hooks: this.hooks }, null, 2);
    await fs.writeFile(this.configPath, content, 'utf-8');
  }

  /**
   * Trigger hooks for an event (fire-and-forget)
   */
  async trigger(event: AgentEvent): Promise<void> {
    // Map EventType to HookEventType
    const hookEventType = this.mapEventType(event.type);
    if (!hookEventType) return;

    const hooks = await this.getEnabledHooks(hookEventType);
    if (hooks.length === 0) return;

    const eventData = this.buildEventData(event);

    // Execute hooks asynchronously (fire-and-forget)
    for (const hook of hooks) {
      this.executeHook(hook, eventData).catch((error) => {
        this.logger.error({ hookId: hook.id, error }, 'Hook execution failed');
      });
    }
  }

  /**
   * Map EventType to HookEventType
   */
  private mapEventType(type: EventType): HookEventType | null {
    const mapping: Partial<Record<EventType, HookEventType>> = {
      [EventType.TOOL_START]: HookEventType.TOOL_START,
      [EventType.TOOL_RESULT]: HookEventType.TOOL_RESULT,
      [EventType.SESSION_START]: HookEventType.SESSION_START,
      [EventType.SESSION_END]: HookEventType.SESSION_END,
      [EventType.RESPONSE]: HookEventType.RESPONSE,
      [EventType.ERROR]: HookEventType.ERROR,
    };
    return mapping[type] ?? null;
  }

  /**
   * Build HookEventData from AgentEvent
   */
  private buildEventData(event: AgentEvent): HookEventData {
    const data: HookEventData = {
      event: event.type,
      timestamp: event.timestamp.toISOString(),
      sessionId: this.sessionId,
    };

    // Extract type-specific fields
    switch (event.type) {
      case EventType.TOOL_START:
        data.toolName = event.data.name as string;
        data.toolArgs = event.data.args as Record<string, unknown>;
        break;
      case EventType.TOOL_RESULT:
        data.toolName = event.data.name as string;
        data.toolSuccess = event.data.success as boolean;
        data.toolResult = event.data.summary as string;
        data.toolError = event.data.error as string;
        break;
      case EventType.SESSION_START:
        data.goal = event.data.goal as string;
        break;
      case EventType.SESSION_END:
        data.success = event.data.success as boolean;
        break;
      case EventType.RESPONSE:
        data.responseText = event.data.content as string;
        break;
      case EventType.ERROR:
        data.errorMessage = event.data.message as string;
        data.errorDetails = event.data.details as string;
        break;
    }

    return data;
  }

  /**
   * Execute a single hook command
   */
  private async executeHook(hook: HookConfig, data: HookEventData): Promise<void> {
    const envVars = hookEventDataToEnvVars(data);
    const jsonData = hookEventDataToJSON(data);

    this.logger.debug({ hookId: hook.id, command: hook.command }, 'Executing hook');

    try {
      await execa('sh', ['-c', hook.command], {
        cwd: this.repoRoot,
        env: { ...process.env, ...envVars },
        input: jsonData,
        timeout: 30000, // 30 second timeout
      });
      this.logger.debug({ hookId: hook.id }, 'Hook executed successfully');
    } catch (error) {
      this.logger.error({ hookId: hook.id, error }, 'Hook execution failed');
    }
  }

  /**
   * Reload hooks from disk
   */
  async reload(): Promise<void> {
    this.loaded = false;
    await this.loadHooks();
  }
}

/**
 * Hook Handler
 *
 * Implements EventHandler to trigger hooks on events.
 */
export class HookHandler implements EventHandler {
  private manager: HookManager;

  constructor(manager: HookManager) {
    this.manager = manager;
  }

  handle(event: AgentEvent): void {
    // Fire-and-forget
    this.manager.trigger(event).catch(() => {
      // Errors are logged by HookManager
    });
  }
}

// Singleton instance
let hookManagerInstance: HookManager | null = null;

/**
 * Get or create the hook manager singleton
 */
export function getHookManager(repoRoot?: string): HookManager {
  if (!hookManagerInstance) {
    hookManagerInstance = new HookManager(repoRoot);
  }
  return hookManagerInstance;
}

/**
 * Create a new hook manager (for testing or isolation)
 */
export function createHookManager(repoRoot?: string, logger?: Logger): HookManager {
  return new HookManager(repoRoot, logger);
}
