import { Injectable, Logger } from "@nestjs/common";
import { Client, Events, GatewayIntentBits, TextChannel, ThreadChannel } from "discord.js";
import { StitcherService } from "../../stitcher/stitcher.service";
import { ChatProxyService } from "../../chat-proxy/chat-proxy.service";
import { PrismaService } from "../../prisma/prisma.service";
import { sanitizeForLogging } from "../../common/utils";
import type {
  IChatProvider,
  ChatMessage,
  ChatCommand,
  ThreadCreateOptions,
  ThreadMessageOptions,
} from "../interfaces";

/**
 * Discord Service - Discord chat platform integration
 *
 * Responsibilities:
 * - Connect to Discord via bot token
 * - Listen for commands in designated channels
 * - Forward commands to stitcher
 * - Route messages in agent channels to specific agents via ChatProxyService
 * - Receive status updates from herald
 * - Post updates to threads
 */
@Injectable()
export class DiscordService implements IChatProvider {
  private readonly logger = new Logger(DiscordService.name);
  private client: Client;
  private connected = false;
  private readonly botToken: string;
  private readonly controlChannelId: string;
  private readonly workspaceId: string;
  private readonly agentChannels = new Map<string, string>();
  private workspaceOwnerId: string | null = null;

  constructor(
    private readonly stitcherService: StitcherService,
    private readonly chatProxyService: ChatProxyService,
    private readonly prisma: PrismaService
  ) {
    this.botToken = process.env.DISCORD_BOT_TOKEN ?? "";
    this.controlChannelId = process.env.DISCORD_CONTROL_CHANNEL_ID ?? "";
    this.workspaceId = process.env.DISCORD_WORKSPACE_ID ?? "";

    // Load agent channel mappings from environment
    this.loadAgentChannels();

    // Initialize Discord client with required intents
    this.client = new Client({
      intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.MessageContent,
      ],
    });

    this.setupEventHandlers();
  }

  /**
   * Load agent channel mappings from environment variables.
   * Format: DISCORD_AGENT_CHANNELS=<channelId>:<agentName>,<channelId>:<agentName>
   * Example: DISCORD_AGENT_CHANNELS=123456:jarvis,789012:builder
   */
  private loadAgentChannels(): void {
    const channelsConfig = process.env.DISCORD_AGENT_CHANNELS ?? "";
    if (!channelsConfig) {
      this.logger.debug("No agent channels configured (DISCORD_AGENT_CHANNELS not set)");
      return;
    }

    const channels = channelsConfig.split(",").map((pair) => pair.trim());
    for (const channel of channels) {
      const [channelId, agentName] = channel.split(":");
      if (channelId && agentName) {
        this.agentChannels.set(channelId.trim(), agentName.trim());
        this.logger.log(`Agent channel mapped: ${channelId.trim()} → ${agentName.trim()}`);
      }
    }
  }

  /**
   * Get the workspace owner's user ID for chat proxy routing.
   * Caches the result after first lookup.
   */
  private async getWorkspaceOwnerId(): Promise<string> {
    if (this.workspaceOwnerId) {
      return this.workspaceOwnerId;
    }

    const workspace = await this.prisma.workspace.findUnique({
      where: { id: this.workspaceId },
      select: { ownerId: true },
    });

    if (!workspace) {
      throw new Error(`Workspace not found: ${this.workspaceId}`);
    }

    this.workspaceOwnerId = workspace.ownerId;
    this.logger.debug(`Workspace owner resolved: ${workspace.ownerId}`);
    return workspace.ownerId;
  }

  /**
   * Setup event handlers for Discord client
   */
  private setupEventHandlers(): void {
    this.client.once(Events.ClientReady, () => {
      this.connected = true;
      const userTag = this.client.user?.tag ?? "Unknown";
      this.logger.log(`Discord bot connected as ${userTag}`);
    });

    this.client.on(Events.MessageCreate, (message) => {
      // Ignore bot messages
      if (message.author.bot) return;

      // Parse message into ChatMessage format
      const chatMessage: ChatMessage = {
        id: message.id,
        channelId: message.channelId,
        authorId: message.author.id,
        authorName: message.author.username,
        content: message.content,
        timestamp: message.createdAt,
        ...(message.channel.isThread() && { threadId: message.channelId }),
      };

      // Check if message is in an agent channel
      const agentName = this.agentChannels.get(message.channelId);
      if (agentName) {
        void this.handleAgentChat(chatMessage, agentName);
        return;
      }

      // Check if message is in control channel for commands
      if (message.channelId !== this.controlChannelId) return;

      // Parse command
      const command = this.parseCommand(chatMessage);
      if (command) {
        void this.handleCommand(command);
      }
    });

    this.client.on(Events.Error, (error: Error) => {
      // Sanitize error before logging to prevent secret exposure
      const sanitizedError = sanitizeForLogging(error);
      this.logger.error("Discord client error:", sanitizedError);
    });
  }

  /**
   * Connect to Discord
   */
  async connect(): Promise<void> {
    if (!this.botToken) {
      throw new Error("DISCORD_BOT_TOKEN is required");
    }

    if (!this.workspaceId) {
      throw new Error("DISCORD_WORKSPACE_ID is required");
    }

    this.logger.log("Connecting to Discord...");
    await this.client.login(this.botToken);
  }

  /**
   * Disconnect from Discord
   */
  async disconnect(): Promise<void> {
    this.logger.log("Disconnecting from Discord...");
    this.connected = false;
    await this.client.destroy();
  }

  /**
   * Check if the provider is connected
   */
  isConnected(): boolean {
    return this.connected;
  }

  /**
   * Send a message to a channel or thread
   */
  async sendMessage(channelId: string, content: string): Promise<void> {
    const channel = await this.client.channels.fetch(channelId);

    if (!channel) {
      throw new Error("Channel not found");
    }

    if (channel.isTextBased()) {
      await (channel as TextChannel).send(content);
    } else {
      throw new Error("Channel is not text-based");
    }
  }

  /**
   * Create a thread for job updates
   */
  async createThread(options: ThreadCreateOptions): Promise<string> {
    const { channelId, name, message } = options;

    const channel = await this.client.channels.fetch(channelId);

    if (!channel) {
      throw new Error("Channel not found");
    }

    if (!channel.isTextBased()) {
      throw new Error("Channel does not support threads");
    }

    const thread = await (channel as TextChannel).threads.create({
      name,
      reason: "Job updates thread",
    });

    // Send initial message to thread
    await thread.send(message);

    return thread.id;
  }

  /**
   * Send a message to a thread
   */
  async sendThreadMessage(options: ThreadMessageOptions): Promise<void> {
    const { threadId, content } = options;

    const thread = await this.client.channels.fetch(threadId);

    if (!thread) {
      throw new Error("Thread not found");
    }

    if (thread.isThread()) {
      await (thread as ThreadChannel).send(content);
    } else {
      throw new Error("Channel is not a thread");
    }
  }

  /**
   * Parse a command from a message
   */
  parseCommand(message: ChatMessage): ChatCommand | null {
    const { content } = message;

    // Check if message mentions @mosaic
    if (!content.toLowerCase().includes("@mosaic")) {
      return null;
    }

    // Extract command and arguments
    const parts = content.trim().split(/\s+/);
    const mosaicIndex = parts.findIndex((part) => part.toLowerCase().includes("@mosaic"));

    if (mosaicIndex === -1 || mosaicIndex === parts.length - 1) {
      return null;
    }

    const commandPart = parts[mosaicIndex + 1];
    if (!commandPart) {
      return null;
    }

    const command = commandPart.toLowerCase();
    const args = parts.slice(mosaicIndex + 2);

    // Valid commands
    const validCommands = ["fix", "status", "cancel", "verbose", "quiet", "help"];

    if (!validCommands.includes(command)) {
      return null;
    }

    return {
      command,
      args,
      message,
    };
  }

  /**
   * Handle a parsed command
   */
  async handleCommand(command: ChatCommand): Promise<void> {
    const { command: cmd, args, message } = command;

    this.logger.log(
      `Handling command: ${cmd} with args: ${args.join(", ")} from ${message.authorName}`
    );

    switch (cmd) {
      case "fix":
        await this.handleFixCommand(args, message);
        break;
      case "status":
        await this.handleStatusCommand(args, message);
        break;
      case "cancel":
        await this.handleCancelCommand(args, message);
        break;
      case "verbose":
        await this.handleVerboseCommand(args, message);
        break;
      case "quiet":
        await this.handleQuietCommand(args, message);
        break;
      case "help":
        await this.handleHelpCommand(args, message);
        break;
      default:
        await this.sendMessage(
          message.channelId,
          `Unknown command: ${cmd}. Type \`@mosaic help\` for available commands.`
        );
    }
  }

  /**
   * Handle fix command - Start a job for an issue
   */
  private async handleFixCommand(args: string[], message: ChatMessage): Promise<void> {
    if (args.length === 0 || !args[0]) {
      await this.sendMessage(message.channelId, "Usage: `@mosaic fix <issue-number>`");
      return;
    }

    const issueNumber = parseInt(args[0], 10);

    if (isNaN(issueNumber)) {
      await this.sendMessage(
        message.channelId,
        "Invalid issue number. Please provide a numeric issue number."
      );
      return;
    }

    // Create thread for job updates
    const threadId = await this.createThread({
      channelId: message.channelId,
      name: `Job #${String(issueNumber)}`,
      message: `Starting job for issue #${String(issueNumber)}...`,
    });

    // Dispatch job to stitcher
    const result = await this.stitcherService.dispatchJob({
      workspaceId: this.workspaceId,
      type: "code-task",
      priority: 10,
      metadata: {
        issueNumber,
        command: "fix",
        channelId: message.channelId,
        threadId: threadId,
        authorId: message.authorId,
        authorName: message.authorName,
      },
    });

    // Send confirmation to thread
    await this.sendThreadMessage({
      threadId,
      channelId: message.channelId,
      content: `Job created: ${result.jobId}\nStatus: ${result.status}\nQueue: ${result.queueName}`,
    });
  }

  /**
   * Handle status command - Get job status
   */
  private async handleStatusCommand(args: string[], message: ChatMessage): Promise<void> {
    if (args.length === 0 || !args[0]) {
      await this.sendMessage(message.channelId, "Usage: `@mosaic status <job-id>`");
      return;
    }

    const jobId = args[0];

    // TODO: Implement job status retrieval from stitcher
    await this.sendMessage(
      message.channelId,
      `Status command not yet implemented for job: ${jobId}`
    );
  }

  /**
   * Handle cancel command - Cancel a running job
   */
  private async handleCancelCommand(args: string[], message: ChatMessage): Promise<void> {
    if (args.length === 0 || !args[0]) {
      await this.sendMessage(message.channelId, "Usage: `@mosaic cancel <job-id>`");
      return;
    }

    const jobId = args[0];

    // TODO: Implement job cancellation in stitcher
    await this.sendMessage(
      message.channelId,
      `Cancel command not yet implemented for job: ${jobId}`
    );
  }

  /**
   * Handle verbose command - Stream full logs to thread
   */
  private async handleVerboseCommand(args: string[], message: ChatMessage): Promise<void> {
    if (args.length === 0 || !args[0]) {
      await this.sendMessage(message.channelId, "Usage: `@mosaic verbose <job-id>`");
      return;
    }

    const jobId = args[0];

    // TODO: Implement verbose logging
    await this.sendMessage(message.channelId, `Verbose mode not yet implemented for job: ${jobId}`);
  }

  /**
   * Handle quiet command - Reduce notifications
   */
  private async handleQuietCommand(_args: string[], message: ChatMessage): Promise<void> {
    // TODO: Implement quiet mode
    await this.sendMessage(
      message.channelId,
      "Quiet mode not yet implemented. Currently showing milestone updates only."
    );
  }

  /**
   * Handle help command - Show available commands
   */
  private async handleHelpCommand(_args: string[], message: ChatMessage): Promise<void> {
    const helpMessage = `
**Available commands:**

\`@mosaic fix <issue>\` - Start job for issue
\`@mosaic status <job>\` - Get job status
\`@mosaic cancel <job>\` - Cancel running job
\`@mosaic verbose <job>\` - Stream full logs to thread
\`@mosaic quiet\` - Reduce notifications
\`@mosaic help\` - Show this help message

**Noise Management:**
• Main channel: Low verbosity (milestones only)
• Job threads: Medium verbosity (step completions)
• DMs: Configurable per user
    `.trim();

    await this.sendMessage(message.channelId, helpMessage);
  }

  /**
   * Handle agent chat - Route message to specific agent via ChatProxyService
   * Messages in agent channels are sent directly to the agent without requiring @mosaic prefix.
   */
  private async handleAgentChat(message: ChatMessage, agentName: string): Promise<void> {
    this.logger.log(
      `Routing message from ${message.authorName} to agent "${agentName}" in channel ${message.channelId}`
    );

    // Ignore empty messages
    if (!message.content.trim()) {
      return;
    }

    try {
      // Get workspace owner ID for routing
      const userId = await this.getWorkspaceOwnerId();

      // Build message history (just the user's message for now)
      const messages = [{ role: "user" as const, content: message.content }];

      // Send typing indicator while waiting for response
      const channel = await this.client.channels.fetch(message.channelId);
      if (channel?.isTextBased()) {
        void (channel as TextChannel).sendTyping();
      }

      // Proxy to agent
      const response = await this.chatProxyService.proxyChat(
        userId,
        messages,
        undefined,
        agentName
      );

      // Stream the response to channel
      await this.streamResponseToChannel(message.channelId, response);

      this.logger.debug(`Agent "${agentName}" response sent to channel ${message.channelId}`);
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.logger.error(`Failed to route message to agent "${agentName}": ${errorMessage}`);
      await this.sendMessage(
        message.channelId,
        `Failed to get response from ${agentName}. Please try again later.`
      );
    }
  }

  /**
   * Stream SSE response from chat proxy and send to Discord channel.
   * Collects the full response and sends as a single message for reliability.
   */
  private async streamResponseToChannel(channelId: string, response: Response): Promise<string> {
    const reader = response.body?.getReader();
    if (!reader) {
      throw new Error("Response body is not readable");
    }

    const decoder = new TextDecoder();
    let fullContent = "";
    let buffer = "";

    try {
      let readResult = await reader.read();
      while (!readResult.done) {
        const { value } = readResult;
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() ?? "";

        for (const line of lines) {
          if (line.startsWith("data: ")) {
            const data = line.slice(6);
            if (data === "[DONE]") continue;

            try {
              const parsed = JSON.parse(data) as {
                choices?: { delta?: { content?: string } }[];
              };
              const content = parsed.choices?.[0]?.delta?.content;
              if (content) {
                fullContent += content;
              }
            } catch {
              // Skip invalid JSON
            }
          }
        }
        readResult = await reader.read();
      }

      // Send the full response to Discord
      if (fullContent.trim()) {
        // Discord has a 2000 character limit, split if needed
        const chunks = this.splitMessageForDiscord(fullContent);
        for (const chunk of chunks) {
          await this.sendMessage(channelId, chunk);
        }
      }

      return fullContent;
    } finally {
      reader.releaseLock();
    }
  }

  /**
   * Split a message into chunks that fit within Discord's 2000 character limit.
   * Tries to split on paragraph or sentence boundaries when possible.
   */
  private splitMessageForDiscord(content: string, maxLength = 2000): string[] {
    if (content.length <= maxLength) {
      return [content];
    }

    const chunks: string[] = [];
    let remaining = content;

    while (remaining.length > maxLength) {
      // Try to find a good break point
      let breakPoint = remaining.lastIndexOf("\n\n", maxLength);
      if (breakPoint < maxLength * 0.5) {
        breakPoint = remaining.lastIndexOf("\n", maxLength);
      }
      if (breakPoint < maxLength * 0.5) {
        breakPoint = remaining.lastIndexOf(". ", maxLength);
      }
      if (breakPoint < maxLength * 0.5) {
        breakPoint = remaining.lastIndexOf(" ", maxLength);
      }
      if (breakPoint < maxLength * 0.5) {
        breakPoint = maxLength - 1;
      }

      chunks.push(remaining.slice(0, breakPoint + 1).trim());
      remaining = remaining.slice(breakPoint + 1).trim();
    }

    if (remaining.length > 0) {
      chunks.push(remaining);
    }

    return chunks;
  }
}
