import sharp from 'sharp';
import { DeleteObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import Client from './Client.mjs';
import prefixedConsole from '../../common/prefixedConsole.mjs';
import ValueCache from '../../common/ValueCache.mjs';

export default class ThreadsClient extends Client {
  key = 'threads';
  name = 'Threads';

  #baseUrl = 'https://graph.threads.net/v1.0';
  #tokenCache = new ValueCache('threads.token');
  #accessToken;

  get console() {
    return this._console ??= prefixedConsole('Social', this.name);
  }

  async canSend() {
    return process.env.THREADS_USER_ID
      && process.env.THREADS_ACCESS_TOKEN
      && process.env.AWS_S3_BUCKET
      && process.env.AWS_S3_ENDPOINT;
  }

  async #getAccessToken() {
    if (this.#accessToken) {
      return this.#accessToken;
    }

    // Try to use a previously refreshed token from cache
    let cached = await this.#tokenCache.getData();
    if (cached) {
      this.#accessToken = cached;
      return this.#accessToken;
    }

    // Fall back to the .env token
    this.#accessToken = process.env.THREADS_ACCESS_TOKEN;
    return this.#accessToken;
  }

  async #refreshToken() {
    let currentToken = await this.#getAccessToken();

    let params = new URLSearchParams({
      grant_type: 'th_refresh_token',
      access_token: currentToken,
    });

    let response = await fetch(`${this.#baseUrl}/refresh_access_token?${params}`);
    let data = await response.json();

    if (data.error) {
      this.console.error('Failed to refresh token:', data.error.message);
      return;
    }

    this.#accessToken = data.access_token;

    // Cache the token with its expiry
    let expires = new Date(Date.now() + data.expires_in * 1000);
    await this.#tokenCache.setData(data.access_token, expires);

    this.console.log(`Token refreshed, expires ${expires.toISOString()}`);
  }

  async send(status, generator) {
    if (!status.media?.length) {
      this.console.error(`No media provided for ${generator.key}`);
      return;
    }

    try {
      // Refresh the token if it hasn't been refreshed in the last 24 hours
      let lastRefresh = await this.#tokenCache.getCachedAt();
      let oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
      if (!lastRefresh || lastRefresh < oneDayAgo) {
        await this.#refreshToken();
      }

      let accessToken = await this.#getAccessToken();
      let jpeg = await sharp(status.media[0].file).jpeg().toBuffer();

      // Upload image to S3 so it's publicly accessible
      let { imageUrl, s3Key } = await this.#uploadImage(jpeg, generator.key);

      try {
        // Create a media container
        let containerId = await this.#createContainer(status.status, imageUrl, accessToken);

        // Wait for the container to finish processing
        await this.#waitForContainer(containerId, accessToken);

        // Publish the container
        await this.#publish(containerId, accessToken);
      } finally {
        // Clean up the temporary image from S3
        await this.#deleteImage(s3Key);
      }
    } catch (error) {
      this.console.error(`Failed to post ${generator.key}:`, error.message);
      throw error;
    }
  }

  async #uploadImage(buffer, key) {
    let s3 = new S3Client({
      endpoint: process.env.AWS_S3_ENDPOINT,
      region: process.env.AWS_REGION,
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      },
    });

    let s3Key = `threads-tmp/${key}.jpg`;

    await s3.send(new PutObjectCommand({
      Bucket: process.env.AWS_S3_BUCKET,
      Key: s3Key,
      Body: buffer,
      ContentType: 'image/jpeg',
      ACL: 'public-read',
    }));

    // Construct the public URL (path-style to avoid SSL issues with dotted bucket names)
    let imageUrl = `${process.env.AWS_S3_ENDPOINT}/${process.env.AWS_S3_BUCKET}/${s3Key}`;
    this.console.log(`Image uploaded: ${imageUrl}`);
    return { imageUrl, s3Key };
  }

  async #deleteImage(s3Key) {
    try {
      let s3 = new S3Client({
        endpoint: process.env.AWS_S3_ENDPOINT,
        region: process.env.AWS_REGION,
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
        },
      });
      await s3.send(new DeleteObjectCommand({
        Bucket: process.env.AWS_S3_BUCKET,
        Key: s3Key,
      }));
      this.console.log(`Temporary image deleted: ${s3Key}`);
    } catch (error) {
      this.console.error('Failed to delete temporary image:', error.message);
    }
  }

  async #createContainer(text, imageUrl, accessToken) {
    let userId = process.env.THREADS_USER_ID;
    let params = new URLSearchParams({
      media_type: 'IMAGE',
      image_url: imageUrl,
      text,
      access_token: accessToken,
    });

    let response = await fetch(`${this.#baseUrl}/${userId}/threads`, {
      method: 'POST',
      body: params,
    });

    let data = await response.json();

    if (data.error) {
      throw new Error(`${data.error.message} (type: ${data.error.type}, code: ${data.error.code}, fbtrace_id: ${data.error.fbtrace_id})`);
    }

    if (!data.id) {
      throw new Error(`Unexpected response: ${JSON.stringify(data)}`);
    }

    return data.id;
  }

  async #waitForContainer(containerId, accessToken) {
    // Poll container status until it's ready (or timeout after 60s)
    let maxAttempts = 12;

    for (let i = 0; i < maxAttempts; i++) {
      let params = new URLSearchParams({
        fields: 'status',
        access_token: accessToken,
      });

      let response = await fetch(`${this.#baseUrl}/${containerId}?${params}`);
      let data = await response.json();

      if (data.status === 'FINISHED') {
        return;
      }

      if (data.status === 'ERROR') {
        throw new Error(`Container processing failed: ${JSON.stringify(data)}`);
      }

      // Wait 5 seconds before checking again
      await new Promise(resolve => setTimeout(resolve, 5000));
    }

    throw new Error('Container processing timed out');
  }

  async #publish(containerId, accessToken) {
    let userId = process.env.THREADS_USER_ID;
    let params = new URLSearchParams({
      creation_id: containerId,
      access_token: accessToken,
    });

    let response = await fetch(`${this.#baseUrl}/${userId}/threads_publish`, {
      method: 'POST',
      body: params,
    });

    let data = await response.json();

    if (data.error) {
      throw new Error(data.error.message);
    }

    return data.id;
  }
}
