import { App } from '@slack/bolt';
import * as fs from 'fs';
import type { SlackConfig } from '../types';

export class SlackProvider {
  private readonly config: SlackConfig;
  private readonly app: App;

  constructor(config: SlackConfig, app: App) {
    this.config = config;
    this.app = app;
  }

  // ── Naming helpers ────────────────────────────────────────────────────────

  getPrefix(): string {
    return this.config.session_name.slice(0, 4);
  }

  getChannelName(cardId: string): string {
    // Slack exige nomes minúsculos; aceita hífen. "CIJIRA-248" → "cijira-248".
    return this.sanitizeChannelName(cardId);
  }

  // Normaliza um nome para as regras do Slack: minúsculas, apenas a-z 0-9 - _,
  // sem hífens repetidos/nas pontas, máx. 80 chars. Hífens são preservados.
  private sanitizeChannelName(name: string): string {
    const slug = name
      .toLowerCase()
      .replace(/[^a-z0-9_-]+/g, '-')
      .replace(/-+/g, '-')
      .replace(/^-+|-+$/g, '')
      .slice(0, 80);
    return slug || 'sdd-card';
  }

  getCommandsChannel(): string {
    return `${this.getPrefix()}-comandos`;
  }

  getLogsChannel(): string {
    return `${this.getPrefix()}-logs`;
  }

  getTasksSession(): string {
    return `${this.getPrefix()}-tasks`;
  }

  // ── Channel management ────────────────────────────────────────────────────

  async createPrivateChannel(name: string): Promise<string> {
    // Sanitiza para um nome válido de canal Slack (minúsculas, sem caracteres
    // especiais). Ex.: "CIJIRA-248" → "cijira-248".
    const channelName = this.sanitizeChannelName(name);
    try {
      const result = await this.app.client.conversations.create({
        token: this.config.bot_token,
        name: channelName,
        is_private: true,
      });

      const channelId = result.channel?.id;
      if (!channelId) {
        throw new Error(
          `SlackProvider.createPrivateChannel: API did not return channel id for name "${channelName}"`
        );
      }
      return channelId;
    } catch (err: unknown) {
      const errorCode = (err as { data?: { error?: string } }).data?.error ?? '';
      // Se o canal já existe, reaproveita em vez de falhar.
      if (errorCode === 'name_taken') {
        const existing = await this.findChannelByName(channelName);
        if (existing) return existing;
      }
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.createPrivateChannel: failed to create channel "${name}" (slug: "${channelName}"): ${msg}`
      );
    }
  }

  // Procura um canal (privado/público) pelo nome exato e retorna seu id, se houver.
  private async findChannelByName(channelName: string): Promise<string | null> {
    try {
      const result = await this.app.client.conversations.list({
        token: this.config.bot_token,
        types: 'private_channel,public_channel',
        limit: 1000,
      });
      const found = (result.channels ?? []).find((c) => c.name === channelName);
      return found?.id ?? null;
    } catch {
      return null;
    }
  }

  async archiveChannel(channelId: string): Promise<void> {
    try {
      await this.app.client.conversations.archive({
        token: this.config.bot_token,
        channel: channelId,
      });
    } catch (err: unknown) {
      const errorCode = (err as { data?: { error?: string } }).data?.error ?? '';
      // already_archived is a no-op — not an error
      if (errorCode === 'already_archived') return;
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.archiveChannel: failed to archive channel "${channelId}": ${msg}`
      );
    }
  }

  // ── Messaging ─────────────────────────────────────────────────────────────

  async postMessage(
    channelId: string,
    text: string,
    options?: { mrkdwn?: boolean }
  ): Promise<string> {
    try {
      const result = await this.app.client.chat.postMessage({
        token: this.config.bot_token,
        channel: channelId,
        text,
        mrkdwn: options?.mrkdwn ?? true,
      });

      if (!result.ts) {
        throw new Error('API did not return message ts');
      }
      return result.ts;
    } catch (err: unknown) {
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.postMessage: failed to post to channel "${channelId}": ${msg}`
      );
    }
  }

  async updateMessage(
    channelId: string,
    ts: string,
    text: string
  ): Promise<void> {
    try {
      await this.app.client.chat.update({
        token: this.config.bot_token,
        channel: channelId,
        ts,
        text,
      });
    } catch (err: unknown) {
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.updateMessage: failed to update message "${ts}" in channel "${channelId}": ${msg}`
      );
    }
  }

  // ── File uploads (Slack v2 API) ───────────────────────────────────────────

  async postFiles(
    channelId: string,
    files: Array<{ path: string; title: string }>
  ): Promise<void> {
    for (const file of files) {
      await this._uploadFile(channelId, file.path, file.title);
    }
  }

  private async _uploadFile(
    channelId: string,
    filePath: string,
    title: string
  ): Promise<void> {
    let fileContent: Buffer;
    try {
      fileContent = fs.readFileSync(filePath);
    } catch (err: unknown) {
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.postFiles: cannot read file "${filePath}": ${msg}`
      );
    }

    const filename = filePath.split(/[/\\]/).pop() ?? title;
    const fileSize = fileContent.length;

    // Step 1 — get upload URL
    let uploadUrl: string;
    let fileId: string;
    try {
      const urlResult = await this.app.client.files.getUploadURLExternal({
        token: this.config.bot_token,
        filename,
        length: fileSize,
      });

      if (!urlResult.upload_url || !urlResult.file_id) {
        throw new Error('API did not return upload_url or file_id');
      }
      uploadUrl = urlResult.upload_url;
      fileId = urlResult.file_id;
    } catch (err: unknown) {
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.postFiles: failed to get upload URL for "${filePath}": ${msg}`
      );
    }

    // Step 2 — PUT the file content to the upload URL
    try {
      const { default: axios } = await import('axios');
      await axios.post(uploadUrl, fileContent, {
        headers: { 'Content-Type': 'application/octet-stream' },
        maxBodyLength: Infinity,
        maxContentLength: Infinity,
      });
    } catch (err: unknown) {
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.postFiles: failed to upload content for "${filePath}": ${msg}`
      );
    }

    // Step 3 — complete the upload and share to channel
    try {
      await this.app.client.files.completeUploadExternal({
        token: this.config.bot_token,
        files: [{ id: fileId, title }],
        channel_id: channelId,
      });
    } catch (err: unknown) {
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.postFiles: failed to complete upload for "${filePath}": ${msg}`
      );
    }
  }

  // ── Members ───────────────────────────────────────────────────────────────

  async addMembers(channelId: string, userIds: string[]): Promise<void> {
    if (userIds.length === 0) return;
    try {
      await this.app.client.conversations.invite({
        token: this.config.bot_token,
        channel: channelId,
        users: userIds.join(','),
      });
    } catch (err: unknown) {
      const errorCode = (err as { data?: { error?: string } }).data?.error ?? '';
      // already_in_channel is not a real error
      if (errorCode === 'already_in_channel') return;
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.addMembers: failed to invite users to channel "${channelId}": ${msg}`
      );
    }
  }

  async getAdminUserIds(): Promise<string[]> {
    const adminIds: string[] = [];
    let cursor: string | undefined;

    try {
      do {
        const result = await this.app.client.users.list({
          token: this.config.bot_token,
          limit: 200,
          cursor,
        });

        for (const member of result.members ?? []) {
          if (member.is_admin && !member.deleted && member.id) {
            adminIds.push(member.id);
          }
        }

        cursor = result.response_metadata?.next_cursor ?? undefined;
      } while (cursor);
    } catch (err: unknown) {
      const msg = (err as Error).message ?? JSON.stringify(err);
      throw new Error(
        `SlackProvider.getAdminUserIds: failed to list workspace members: ${msg}`
      );
    }

    return adminIds;
  }

  // ── Admin log channel ─────────────────────────────────────────────────────

  async logToAdminChannel(
    message: string,
    files?: Array<{ path: string; title: string }>
  ): Promise<void> {
    const logsChannel = this.getLogsChannel();

    // Resolve the channel id by name — the logs channel may be referenced by name
    let channelId: string;
    try {
      channelId = await this._resolveChannelId(logsChannel);
    } catch {
      // If the logs channel doesn't exist yet, log to stdout and move on
      console.warn(
        `SlackProvider.logToAdminChannel: logs channel "${logsChannel}" not found — message dropped to stdout: ${message}`
      );
      return;
    }

    await this.postMessage(channelId, message);

    if (files && files.length > 0) {
      await this.postFiles(channelId, files);
    }
  }

  // ── Internal helpers ──────────────────────────────────────────────────────

  private async _resolveChannelId(channelName: string): Promise<string> {
    let cursor: string | undefined;

    do {
      const result = await this.app.client.conversations.list({
        token: this.config.bot_token,
        types: 'private_channel,public_channel',
        limit: 200,
        cursor,
      });

      for (const ch of result.channels ?? []) {
        if (ch.name === channelName && ch.id) {
          return ch.id;
        }
      }

      cursor = result.response_metadata?.next_cursor ?? undefined;
    } while (cursor);

    throw new Error(
      `SlackProvider._resolveChannelId: channel "${channelName}" not found in workspace`
    );
  }
}
