import { forwardRef, Inject, Injectable, Logger, Optional } from '@nestjs/common';
import type { QueueHandle } from '@mosaicstack/queue';
import type { Brain } from '@mosaicstack/brain';
import type { SlashCommandPayload, SlashCommandResultPayload } from '@mosaicstack/types';
import { AgentService } from '../agent/agent.service.js';
import type { ActorTenantScope } from '../auth/session-scope.js';
import { ChatGateway } from '../chat/chat.gateway.js';
import { SessionGCService } from '../gc/session-gc.service.js';
import { SystemOverrideService } from '../preferences/system-override.service.js';
import { ReloadService } from '../reload/reload.service.js';
import { McpClientService } from '../mcp-client/mcp-client.service.js';
import { BRAIN } from '../brain/brain.tokens.js';
import { COMMANDS_REDIS } from './commands.tokens.js';
import { CommandAuthorizationService } from './command-authorization.service.js';
import { CommandRegistryService } from './command-registry.service.js';

@Injectable()
export class CommandExecutorService {
  private readonly logger = new Logger(CommandExecutorService.name);

  constructor(
    @Inject(CommandRegistryService) private readonly registry: CommandRegistryService,
    @Inject(AgentService) private readonly agentService: AgentService,
    @Inject(SystemOverrideService) private readonly systemOverride: SystemOverrideService,
    @Inject(SessionGCService) private readonly sessionGC: SessionGCService,
    @Inject(COMMANDS_REDIS) private readonly redis: QueueHandle['redis'],
    @Inject(BRAIN) private readonly brain: Brain,
    @Optional()
    @Inject(forwardRef(() => ReloadService))
    private readonly reloadService: ReloadService | null,
    @Optional()
    @Inject(forwardRef(() => ChatGateway))
    private readonly chatGateway: ChatGateway | null,
    @Optional()
    @Inject(McpClientService)
    private readonly mcpClient: McpClientService | null,
    @Optional()
    @Inject(CommandAuthorizationService)
    private readonly authorization: CommandAuthorizationService | null = null,
  ) {}

  async execute(
    payload: SlashCommandPayload,
    scope: ActorTenantScope,
  ): Promise<SlashCommandResultPayload> {
    const { command, args, conversationId } = payload;
    const userId = scope.userId;

    const def = this.registry.getManifest().commands.find((c) => c.name === command);
    if (!def) {
      return {
        command,
        conversationId,
        success: false,
        message: `Unknown command: /${command}`,
      };
    }

    const authorization = await this.authorization?.authorize(
      def,
      payload,
      userId,
      payload.approvalId,
    );
    if (authorization && !authorization.allowed) {
      return { command, conversationId, success: false, message: authorization.reason };
    }

    try {
      switch (command) {
        case 'model':
          return await this.handleModel(args ?? null, conversationId, scope);
        case 'thinking':
          return await this.handleThinking(args ?? null, conversationId);
        case 'system':
          return await this.handleSystem(args ?? null, conversationId, scope);
        case 'new':
          return {
            command,
            conversationId,
            success: true,
            message: 'Start a new conversation by selecting New Conversation.',
          };
        case 'clear':
          return {
            command,
            conversationId,
            success: true,
            message: 'Conversation display cleared.',
          };
        case 'compact':
          return {
            command,
            conversationId,
            success: true,
            message: 'Context compaction requested.',
          };
        case 'retry':
          return {
            command,
            conversationId,
            success: true,
            message: 'Retry last message requested.',
          };
        case 'gc':
          // Global retention requires a separate, authorized and audited job.
          // Session cleanup is performed only through the session lifecycle.
          return {
            command: 'gc',
            success: false,
            message: 'Global GC is disabled pending an authorized retention job.',
            conversationId,
          };
        case 'agent':
          return await this.handleAgent(args ?? null, conversationId, scope);
        case 'provider':
          return await this.handleProvider(args ?? null, userId, conversationId);
        case 'mission':
          return await this.handleMission(args ?? null, conversationId, userId);
        case 'prdy':
          return {
            command: 'prdy',
            success: true,
            message:
              'PRD wizard: run `mosaic prdy` in your project workspace to create or update a PRD.',
            conversationId,
          };
        case 'tools':
          return await this.handleTools(conversationId, userId);
        case 'mcp':
          return await this.handleMcp(args ?? null, conversationId);
        case 'reload': {
          if (!this.reloadService) {
            return {
              command: 'reload',
              conversationId,
              success: false,
              message: 'ReloadService is not available.',
            };
          }
          const reloadResult = await this.reloadService.reload('command');
          this.chatGateway?.broadcastReload(reloadResult);
          return {
            command: 'reload',
            success: true,
            message: reloadResult.message,
            conversationId,
          };
        }
        default:
          return {
            command,
            conversationId,
            success: false,
            message: `Command /${command} is not yet implemented.`,
          };
      }
    } catch (err) {
      this.logger.error(`Command /${command} failed: ${err}`);
      return { command, conversationId, success: false, message: String(err) };
    }
  }

  async createApproval(payload: SlashCommandPayload, scope: ActorTenantScope) {
    const def = this.registry
      .getManifest()
      .commands.find((command) => command.name === payload.command);
    if (!def || !this.authorization) return null;
    return this.authorization.createApproval(def, payload, scope.userId);
  }

  private async handleModel(
    args: string | null,
    conversationId: string,
    scope: ActorTenantScope,
  ): Promise<SlashCommandResultPayload> {
    if (!args || args.trim().length === 0) {
      // Show current override or usage hint
      const currentOverride = this.chatGateway?.getModelOverride(conversationId, scope);
      if (currentOverride) {
        return {
          command: 'model',
          conversationId,
          success: true,
          message: `Current model override: "${currentOverride}". Use /model <name> to change or /model clear to reset.`,
        };
      }
      return {
        command: 'model',
        conversationId,
        success: true,
        message:
          'Usage: /model <model-name>  — sets a per-session model override (bypasses routing). Use /model clear to reset.',
      };
    }

    const modelName = args.trim();

    // /model clear removes the override and re-enables automatic routing
    if (modelName === 'clear') {
      this.chatGateway?.setModelOverride(conversationId, null, scope);
      return {
        command: 'model',
        conversationId,
        success: true,
        message: 'Model override cleared. Automatic routing will be used for new sessions.',
      };
    }

    // Set the sticky per-session override (M4-007)
    this.chatGateway?.setModelOverride(conversationId, modelName, scope);

    const session = this.agentService.getSession(conversationId, scope);
    if (!session) {
      return {
        command: 'model',
        conversationId,
        success: true,
        message: `Model override set to "${modelName}". Will apply when a new session starts for this conversation.`,
      };
    }
    return {
      command: 'model',
      conversationId,
      success: true,
      message: `Model override set to "${modelName}". The override is active for this conversation and will be used on the next message if a new session is needed.`,
    };
  }

  private async handleThinking(
    args: string | null,
    conversationId: string,
  ): Promise<SlashCommandResultPayload> {
    const level = args?.toLowerCase();
    if (!level || !['none', 'low', 'medium', 'high', 'auto'].includes(level)) {
      return {
        command: 'thinking',
        conversationId,
        success: true,
        message: 'Usage: /thinking <none|low|medium|high|auto>',
      };
    }
    return {
      command: 'thinking',
      conversationId,
      success: true,
      message: `Thinking level set to "${level}".`,
    };
  }

  private async handleSystem(
    args: string | null,
    conversationId: string,
    scope: ActorTenantScope,
  ): Promise<SlashCommandResultPayload> {
    if (!args || args.trim().length === 0) {
      // Clear the override when called with no args
      await this.systemOverride.clear(conversationId, scope);
      return {
        command: 'system',
        conversationId,
        success: true,
        message: 'Session system prompt override cleared.',
      };
    }

    await this.systemOverride.set(conversationId, args.trim(), scope);
    return {
      command: 'system',
      conversationId,
      success: true,
      message: `Session system prompt override set (expires in 5 minutes of inactivity).`,
    };
  }

  private async handleAgent(
    args: string | null,
    conversationId: string,
    scope: ActorTenantScope,
  ): Promise<SlashCommandResultPayload> {
    const userId = scope.userId;
    if (!args) {
      return {
        command: 'agent',
        success: true,
        message:
          'Usage: /agent <agent-id> | /agent list | /agent new <name> to create a new agent.',
        conversationId,
      };
    }

    if (args === 'list') {
      return {
        command: 'agent',
        success: true,
        message: 'Agent listing: use the web dashboard for full agent management.',
        conversationId,
      };
    }

    // M5-006: /agent new <name> — create a new agent config via brain.agents.create()
    if (args.startsWith('new')) {
      const namePart = args.slice(3).trim();
      if (!namePart) {
        return {
          command: 'agent',
          success: false,
          message: 'Usage: /agent new <name> — provide a name for the new agent.',
          conversationId,
        };
      }

      try {
        const defaultProvider = process.env['DEFAULT_PROVIDER'] ?? 'anthropic';
        const defaultModel = process.env['DEFAULT_MODEL'] ?? 'claude-sonnet-4-5-20251001';

        const newAgent = await this.brain.agents.create({
          name: namePart,
          provider: defaultProvider,
          model: defaultModel,
          status: 'idle',
          ownerId: userId,
          isSystem: false,
        });

        this.logger.log(`Created new agent "${newAgent.name}" (${newAgent.id}) for user ${userId}`);

        return {
          command: 'agent',
          success: true,
          message: `Agent "${newAgent.name}" created with ID: ${newAgent.id}. Configure it via the web dashboard.`,
          conversationId,
          data: { agentId: newAgent.id, agentName: newAgent.name },
        };
      } catch (err) {
        this.logger.error(`Failed to create agent: ${err}`);
        return {
          command: 'agent',
          success: false,
          message: `Failed to create agent: ${String(err)}`,
          conversationId,
        };
      }
    }

    // M5-003: Look up agent by name (or ID) and apply to session mid-conversation
    const agentName = args.trim();
    try {
      // Try lookup by name first; fall back to ID-based lookup
      let agentConfig = await this.brain.agents.findByName(agentName);
      if (!agentConfig) {
        // Try by ID (UUID-style input)
        agentConfig = await this.brain.agents.findById(agentName);
      }

      if (!agentConfig) {
        return {
          command: 'agent',
          success: false,
          message: `Agent "${agentName}" not found. Use /agent list to see available agents.`,
          conversationId,
        };
      }

      // Apply the agent config to the live session and emit session:info (M5-003)
      this.agentService.applyAgentConfig(
        conversationId,
        agentConfig.id,
        agentConfig.name,
        scope,
        agentConfig.model ?? undefined,
      );

      // Broadcast updated session:info so TUI TopBar reflects new agent/model
      this.chatGateway?.broadcastSessionInfo(conversationId, scope, {
        agentName: agentConfig.name,
      });

      this.logger.log(
        `Agent switched to "${agentConfig.name}" (${agentConfig.id}) for conversation ${conversationId} (M5-003)`,
      );

      return {
        command: 'agent',
        success: true,
        message: `Switched to agent "${agentConfig.name}". System prompt and tools applied. Model: ${agentConfig.model ?? 'default'}.`,
        conversationId,
        data: { agentId: agentConfig.id, agentName: agentConfig.name, model: agentConfig.model },
      };
    } catch (err) {
      this.logger.error(`Failed to switch agent "${agentName}": ${err}`);
      return {
        command: 'agent',
        success: false,
        message: `Failed to switch agent: ${String(err)}`,
        conversationId,
      };
    }
  }

  private async handleProvider(
    args: string | null,
    userId: string,
    conversationId: string,
  ): Promise<SlashCommandResultPayload> {
    if (!args) {
      return {
        command: 'provider',
        success: true,
        message: 'Usage: /provider list | /provider login <name> | /provider logout <name>',
        conversationId,
      };
    }

    const spaceIdx = args.indexOf(' ');
    const subcommand = spaceIdx >= 0 ? args.slice(0, spaceIdx) : args;
    const providerName = spaceIdx >= 0 ? args.slice(spaceIdx + 1).trim() : '';

    switch (subcommand) {
      case 'list':
        return {
          command: 'provider',
          success: true,
          message: 'Use the web dashboard to manage providers.',
          conversationId,
        };

      case 'login': {
        if (!providerName) {
          return {
            command: 'provider',
            success: false,
            message: 'Usage: /provider login <provider-name>',
            conversationId,
          };
        }
        const pollToken = crypto.randomUUID();
        const tokenDigest = await crypto.subtle.digest(
          'SHA-256',
          new TextEncoder().encode(pollToken),
        );
        const tokenHash = Array.from(new Uint8Array(tokenDigest), (byte: number): string =>
          byte.toString(16).padStart(2, '0'),
        ).join('');
        const key = `mosaic:auth:poll:${tokenHash}`;
        // Persist only a short-lived token digest. The raw token is delivered only by
        // the authenticated dashboard flow, never in chat output or command metadata.
        await this.redis.set(
          key,
          JSON.stringify({ status: 'pending', provider: providerName, userId }),
          'EX',
          300,
        );
        return {
          command: 'provider',
          success: true,
          message: `Provider login for ${providerName} is ready. Continue in the authenticated dashboard.`,
          conversationId,
          data: { provider: providerName },
        };
      }

      case 'logout': {
        if (!providerName) {
          return {
            command: 'provider',
            success: false,
            message: 'Usage: /provider logout <provider-name>',
            conversationId,
          };
        }
        return {
          command: 'provider',
          success: true,
          message: `Logout from ${providerName}: use the web dashboard to revoke provider tokens.`,
          conversationId,
        };
      }

      default:
        return {
          command: 'provider',
          success: false,
          message: `Unknown subcommand: ${subcommand}. Use list, login, or logout.`,
          conversationId,
        };
    }
  }

  private async handleMission(
    args: string | null,
    conversationId: string,
    _userId: string,
  ): Promise<SlashCommandResultPayload> {
    if (!args || args === 'status') {
      // TODO: fetch active mission from DB when MissionsService is available
      return {
        command: 'mission',
        success: true,
        message: 'Mission status: use the web dashboard for full mission management.',
        conversationId,
      };
    }

    if (args.startsWith('set ')) {
      const missionId = args.slice(4).trim();
      return {
        command: 'mission',
        success: true,
        message: `Mission set to ${missionId}. Session context updated.`,
        conversationId,
      };
    }

    return {
      command: 'mission',
      success: true,
      message: 'Usage: /mission [status|set <id>|list|tasks]',
      conversationId,
    };
  }

  private async handleTools(
    conversationId: string,
    _userId: string,
  ): Promise<SlashCommandResultPayload> {
    // TODO: fetch tool list from active agent session
    return {
      command: 'tools',
      success: true,
      message:
        'Available tools depend on the active agent configuration. Use the web dashboard to configure tool access.',
      conversationId,
    };
  }

  private async handleMcp(
    args: string | null,
    conversationId: string,
  ): Promise<SlashCommandResultPayload> {
    if (!this.mcpClient) {
      return {
        command: 'mcp',
        conversationId,
        success: false,
        message: 'MCP client service is not available.',
      };
    }

    const action = args?.trim().split(/\s+/)[0] ?? 'status';

    switch (action) {
      case 'status':
      case 'servers': {
        const statuses = this.mcpClient.getServerStatuses();
        if (statuses.length === 0) {
          return {
            command: 'mcp',
            conversationId,
            success: true,
            message:
              'No MCP servers configured. Set MCP_SERVERS env var to connect external tool servers.',
          };
        }
        const lines = ['MCP Server Status:\n'];
        for (const s of statuses) {
          const status = s.connected ? '✓ connected' : '✗ disconnected';
          lines.push(`  ${s.name}: ${status}`);
          lines.push(`    URL: ${s.url}`);
          lines.push(`    Tools: ${s.toolCount}`);
          if (s.error) lines.push(`    Error: ${s.error}`);
          lines.push('');
        }
        const tools = this.mcpClient.getToolDefinitions();
        if (tools.length > 0) {
          lines.push(`Total bridged tools: ${tools.length}`);
          lines.push(`Tool names: ${tools.map((t) => t.name).join(', ')}`);
        }
        return {
          command: 'mcp',
          conversationId,
          success: true,
          message: lines.join('\n'),
        };
      }

      case 'reconnect': {
        const serverName = args?.trim().split(/\s+/).slice(1).join(' ');
        if (!serverName) {
          return {
            command: 'mcp',
            conversationId,
            success: false,
            message: 'Usage: /mcp reconnect <server-name>',
          };
        }
        try {
          await this.mcpClient.reconnectServer(serverName);
          return {
            command: 'mcp',
            conversationId,
            success: true,
            message: `MCP server "${serverName}" reconnected successfully.`,
          };
        } catch (err) {
          return {
            command: 'mcp',
            conversationId,
            success: false,
            message: `Failed to reconnect MCP server "${serverName}": ${err instanceof Error ? err.message : String(err)}`,
          };
        }
      }

      default:
        return {
          command: 'mcp',
          conversationId,
          success: false,
          message: `Unknown MCP action: "${action}". Use: /mcp status, /mcp servers, /mcp reconnect <name>`,
        };
    }
  }
}
