import { readFileSync } from 'fs';
import type { IncomingMessage, ServerResponse } from 'http';

import { google } from 'googleapis';

import type {
  InboundMessage,
  OutboundMessage,
  ChannelAdapter,
  ChannelStatus,
} from '../types.js';
import { shouldIgnoreForMissingMention, splitMessage } from '../utils.js';
import { readRequestBody } from '../../utils/httpBody.js';

export interface GoogleChatAdapterConfig {
  serviceAccountKeyFile?: string;
  serviceAccountJson?: string;
  webhookToken?: string;
  allowedSpaceNames?: string[];
  requireMention?: boolean;
}

interface GoogleChatEvent {
  type?: string;
  token?: string;
  message?: {
    name?: string;
    text?: string;
    argumentText?: string;
    sender?: { name?: string; displayName?: string; type?: string };
    space?: { name?: string; type?: string };
    thread?: { name?: string };
  };
}

const MAX_MESSAGE_LENGTH = 3500;
const CHAT_BOT_SCOPE = 'https://www.googleapis.com/auth/chat.bot';

export class GoogleChatAdapter implements ChannelAdapter {
  readonly name = 'google_chat';

  private _status: ChannelStatus = 'disconnected';
  private messageHandler?: (msg: InboundMessage) => void;
  private accessTokenCache: { token: string; expiresAt: number } | null = null;

  constructor(private readonly config: GoogleChatAdapterConfig) {}

  get status(): ChannelStatus {
    return this._status;
  }

  onMessage(handler: (msg: InboundMessage) => void): void {
    this.messageHandler = handler;
  }

  async connect(): Promise<void> {
    this._status = 'connected';
  }

  async disconnect(): Promise<void> {
    this._status = 'disconnected';
  }

  async send(peerId: string, message: OutboundMessage): Promise<void> {
    if (this._status !== 'connected') {
      throw new Error('Google Chat adapter not connected');
    }

    const ref = this.decodePeerRef(peerId);
    const text = message.text.trim();
    if (!text) {
      return;
    }

    const chunks = text.length > MAX_MESSAGE_LENGTH ? splitMessage(text, MAX_MESSAGE_LENGTH) : [text];
    for (const chunk of chunks) {
      await this.sendMessage(ref.spaceName, ref.threadName, chunk);
    }
  }

  async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
    let body = '';
    try {
      body = await readRequestBody(req, { maxBytes: 1_048_576 });
    } catch {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Invalid request body' }));
      return;
    }

    let event: GoogleChatEvent;
    try {
      event = JSON.parse(body) as GoogleChatEvent;
    } catch {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Invalid JSON' }));
      return;
    }

    if (this.config.webhookToken && event.token !== this.config.webhookToken) {
      res.writeHead(401, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Invalid webhook token' }));
      return;
    }

    await this.handleEvent(event);
    res.writeHead(202, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ accepted: true }));
  }

  async handleEvent(event: GoogleChatEvent): Promise<void> {
    if (!this.messageHandler) {
      return;
    }
    if (event.type !== 'MESSAGE') {
      return;
    }

    const message = event.message;
    const spaceName = message?.space?.name?.trim();
    if (!spaceName) {
      return;
    }
    if (this.config.allowedSpaceNames && this.config.allowedSpaceNames.length > 0) {
      if (!this.config.allowedSpaceNames.includes(spaceName)) {
        return;
      }
    }

    const isDm = (message?.space?.type ?? '').toUpperCase() === 'DM';
    const argumentText = (message?.argumentText ?? '').trim();
    const rawText = argumentText || (message?.text ?? '').trim();
    if (!rawText) {
      return;
    }

    if (shouldIgnoreForMissingMention({
      requireMention: this.config.requireMention,
      defaultRequireMention: true,
      mentionsBot: isDm || argumentText.length > 0,
    })) {
      return;
    }

    const senderId = message?.sender?.name?.trim();
    if (!senderId) {
      return;
    }
    const threadName = message?.thread?.name?.trim() || undefined;
    const peerRef = this.encodePeerRef({ spaceName, threadName });

    this.messageHandler({
      id: message?.name ?? `google-chat-${Date.now()}`,
      channel: 'google_chat',
      senderId: `${spaceName}:${senderId}`,
      senderName: message?.sender?.displayName,
      text: rawText,
      timestamp: Date.now(),
      metadata: {
        spaceName,
        threadName,
        replyPeerId: peerRef,
      },
    });
  }

  private async sendMessage(spaceName: string, threadName: string | undefined, text: string): Promise<void> {
    const token = await this.getAccessToken();
    const endpoint = `https://chat.googleapis.com/v1/${spaceName}/messages`;

    const body: Record<string, unknown> = { text };
    if (threadName) {
      body.thread = { name: threadName };
    }

    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    if (!response.ok) {
      const responseBody = await response.text().catch(() => '');
      throw new Error(`Google Chat send failed (${response.status}): ${responseBody}`);
    }
  }

  private async getAccessToken(): Promise<string> {
    const now = Date.now();
    if (this.accessTokenCache && this.accessTokenCache.expiresAt > now + 30_000) {
      return this.accessTokenCache.token;
    }

    const credentials = this.resolveServiceAccountCredentials();
    const auth = new google.auth.GoogleAuth({
      credentials,
      scopes: [CHAT_BOT_SCOPE],
    });
    const client = await auth.getClient();
    const tokenResponse = await client.getAccessToken();
    const token = typeof tokenResponse === 'string' ? tokenResponse : tokenResponse.token;
    if (!token) {
      throw new Error('Google Chat auth failed: missing access token');
    }

    this.accessTokenCache = {
      token,
      expiresAt: now + 50 * 60 * 1000,
    };
    return token;
  }

  private resolveServiceAccountCredentials(): Record<string, unknown> {
    if (this.config.serviceAccountJson) {
      return JSON.parse(this.config.serviceAccountJson) as Record<string, unknown>;
    }
    if (this.config.serviceAccountKeyFile) {
      const raw = readFileSync(this.config.serviceAccountKeyFile, 'utf8');
      return JSON.parse(raw) as Record<string, unknown>;
    }
    const envJson = process.env.GOOGLE_CHAT_SERVICE_ACCOUNT_JSON;
    if (envJson && envJson.trim().length > 0) {
      return JSON.parse(envJson) as Record<string, unknown>;
    }
    throw new Error(
      'Google Chat credentials missing: set google_chat.service_account_json, google_chat.service_account_key_file, or GOOGLE_CHAT_SERVICE_ACCOUNT_JSON',
    );
  }

  private encodePeerRef(ref: { spaceName: string; threadName?: string }): string {
    const payload = Buffer.from(JSON.stringify(ref), 'utf8').toString('base64url');
    return `gchat:${payload}`;
  }

  private decodePeerRef(peerId: string): { spaceName: string; threadName?: string } {
    if (!peerId.startsWith('gchat:')) {
      throw new Error('Google Chat peerId must use gchat:<base64url-json> format');
    }
    const encoded = peerId.slice('gchat:'.length);
    let decoded = '';
    try {
      decoded = Buffer.from(encoded, 'base64url').toString('utf8');
    } catch {
      throw new Error('Invalid Google Chat peer reference encoding');
    }
    let parsed: { spaceName?: string; threadName?: string } = {};
    try {
      parsed = JSON.parse(decoded) as { spaceName?: string; threadName?: string };
    } catch {
      throw new Error('Invalid Google Chat peer reference payload');
    }
    const spaceName = parsed.spaceName;
    if (!spaceName) {
      throw new Error('Google Chat peer reference missing spaceName');
    }
    return {
      spaceName,
      threadName: parsed.threadName,
    };
  }
}
