/**
 * Threads (Meta Threads) Publisher Service
 *
 * Publishes content to a Threads account via the Threads Graph API two-step
 * Content Publishing protocol (mirrors the Instagram publisher):
 *   1. Create a media container (POST /{threads-user}/threads).
 *   2. (video/carousel) Poll status until FINISHED.
 *   3. Publish the container (POST /{threads-user}/threads_publish).
 *
 * IMPORTANT — the public-URL constraint: Threads fetches media from a URL; it
 * does not accept binary uploads. Uploaded files are served from
 * `${PUBLIC_BASE_URL}/uploads/social-media/...` (must be reachable over public
 * HTTPS). Text-only posts need no media and no PUBLIC_BASE_URL.
 *
 * Publishing isolation: the token is always resolved via
 * getFreshToken(companyId, createdBy, accountRef) — the owning admin's account.
 * A job never falls back to another connection.
 */

import path from 'path';
import { getFreshToken, getThreadsCredentials } from './threadsAuth';
import type { ISocialMediaPublication } from '../../models/SocialMediaPublication';

const GRAPH_BASE = 'https://graph.threads.net';
const MAX_ATTEMPTS = 3;

// Inline readiness poll before deferring to the worker (keeps text/images instant)
const INLINE_POLL_TRIES = 3;
const INLINE_POLL_DELAY_MS = 2000;

// ============================================
// HELPERS
// ============================================

async function graphVersion(): Promise<string> {
  const creds = await getThreadsCredentials();
  return creds?.graphVersion || process.env.THREADS_GRAPH_VERSION || 'v1.0';
}

function publicBaseUrl(): string {
  return (process.env.PUBLIC_BASE_URL || process.env.APP_PUBLIC_URL || '').replace(/\/$/, '');
}

/** Build the public HTTPS URL Threads will fetch for a stored media file. */
function publicUrlFor(filePath: string): string {
  const base = publicBaseUrl();
  const rel = filePath.startsWith('/') ? filePath : `/${filePath}`;
  return `${base}${rel}`;
}

function isVideoPath(filePath: string): boolean {
  const ext = path.extname(filePath).toLowerCase();
  return ['.mp4', '.mov', '.avi', '.webm', '.m4v'].includes(ext);
}

function recordError(publication: ISocialMediaPublication, code: string, message: string): void {
  const error = { code, message: message.slice(0, 500), at: new Date() };
  publication.lastError = error as any;
  publication.errorHistory.push(error as any);
}

async function failAttempt(publication: ISocialMediaPublication, code: string, message: string, retryable: boolean): Promise<void> {
  recordError(publication, code, message);
  publication.attemptCount += 1;
  publication.workerLockedAt = null as any;

  if (retryable && publication.attemptCount < MAX_ATTEMPTS) {
    publication.status = 'queued';
    publication.nextAttemptAt = new Date(Date.now() + Math.pow(4, publication.attemptCount) * 30 * 1000);
  } else {
    publication.status = 'failed';
    publication.nextAttemptAt = null as any;
  }
  await publication.save();
}

const TOKEN_ERROR_CODES = new Set([190, 10]);
const PERMISSION_ERROR_CODES = new Set([200, 803, 3, 100]);
const RATE_LIMIT_CODES = new Set([4, 17, 32, 613, 80001]);

function classifyError(status: number, body: string): { code: string; message: string; retryable: boolean } {
  let fbCode: number | undefined;
  let message = `Threads API error (HTTP ${status})`;
  try {
    const parsed = JSON.parse(body);
    if (parsed?.error) {
      fbCode = parsed.error.code;
      message = parsed.error.error_user_msg || parsed.error.message || message;
    } else {
      message = parsed?.error_message || message;
    }
  } catch {
    // keep default
  }

  if (fbCode !== undefined && TOKEN_ERROR_CODES.has(fbCode)) return { code: 'invalid_token', message, retryable: false };
  if (fbCode !== undefined && PERMISSION_ERROR_CODES.has(fbCode)) return { code: 'permission_missing', message, retryable: false };
  if (fbCode !== undefined && RATE_LIMIT_CODES.has(fbCode)) return { code: 'rate_limited', message, retryable: true };
  if (status === 401 || status === 403) return { code: 'permission_missing', message, retryable: false };
  if (status === 429) return { code: 'rate_limited', message, retryable: true };
  if (status >= 500) return { code: 'threads_server_error', message, retryable: true };
  return { code: 'threads_api_error', message, retryable: false };
}

// ============================================
// GRAPH CALLS
// ============================================

/** Create a media container. Returns the container id. */
async function createContainer(version: string, threadsUserId: string, accessToken: string, params: Record<string, string>): Promise<string> {
  const form = new URLSearchParams({ ...params, access_token: accessToken });
  const response = await fetch(`${GRAPH_BASE}/${version}/${threadsUserId}/threads`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: form.toString(),
  });
  if (!response.ok) {
    const info = classifyError(response.status, await response.text());
    throw Object.assign(new Error(info.message), info);
  }
  const data: any = await response.json();
  if (!data.id) throw Object.assign(new Error('Threads did not return a container id'), { code: 'container_failed', retryable: false });
  return data.id;
}

/** Poll a container's processing state. */
async function getContainerStatus(version: string, containerId: string, accessToken: string): Promise<string> {
  const response = await fetch(`${GRAPH_BASE}/${version}/${containerId}?fields=status,error_message&access_token=${encodeURIComponent(accessToken)}`);
  if (!response.ok) return 'IN_PROGRESS';
  const data: any = await response.json();
  return data.status || 'IN_PROGRESS'; // EXPIRED | ERROR | FINISHED | IN_PROGRESS | PUBLISHED
}

/** Publish a finished container. Returns the Threads media id. */
async function publishContainer(version: string, threadsUserId: string, accessToken: string, creationId: string): Promise<string> {
  const form = new URLSearchParams({ creation_id: creationId, access_token: accessToken });
  const response = await fetch(`${GRAPH_BASE}/${version}/${threadsUserId}/threads_publish`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: form.toString(),
  });
  if (!response.ok) {
    const info = classifyError(response.status, await response.text());
    throw Object.assign(new Error(info.message), info);
  }
  const data: any = await response.json();
  if (!data.id) throw Object.assign(new Error('Publish returned no media id'), { code: 'publish_failed', retryable: true });
  return data.id;
}

/** Best-effort quota check (250 published posts / 24h per Threads account). */
async function isQuotaExhausted(version: string, threadsUserId: string, accessToken: string): Promise<boolean> {
  try {
    const response = await fetch(`${GRAPH_BASE}/${version}/${threadsUserId}/threads_publishing_limit?fields=quota_usage,config&access_token=${encodeURIComponent(accessToken)}`);
    if (!response.ok) return false;
    const data: any = await response.json();
    const row = data?.data?.[0];
    if (!row) return false;
    const used = row.quota_usage ?? 0;
    const total = row.config?.quota_total ?? 250;
    return used >= total;
  } catch {
    return false;
  }
}

async function fetchPermalink(version: string, mediaId: string, accessToken: string): Promise<string | null> {
  try {
    const response = await fetch(`${GRAPH_BASE}/${version}/${mediaId}?fields=permalink&access_token=${encodeURIComponent(accessToken)}`);
    if (!response.ok) return null;
    const data: any = await response.json();
    return data.permalink || null;
  } catch {
    return null;
  }
}

// ============================================
// CONTAINER BUILDERS PER POST TYPE
// ============================================

/**
 * Create the publishable container for a publication and return its id plus
 * whether it needs async processing before publishing.
 */
async function buildPublishableContainer(version: string, threadsUserId: string, accessToken: string, publication: ISocialMediaPublication): Promise<{ containerId: string; needsProcessing: boolean }> {
  const files = publication.mediaFilePaths || [];
  const urls = files.map(publicUrlFor);
  publication.mediaPublicUrls = urls;
  const text = publication.message || '';
  const postType = publication.postType || 'text';
  const replyControl = publication.replyControl && publication.replyControl !== 'everyone' ? publication.replyControl : '';

  const withCommon = (params: Record<string, string>): Record<string, string> => {
    if (text) params.text = text;
    if (replyControl) params.reply_control = replyControl;
    return params;
  };

  if (postType === 'text') {
    const params = withCommon({ media_type: 'TEXT' });
    if (publication.linkAttachment) params.link_attachment = publication.linkAttachment;
    const containerId = await createContainer(version, threadsUserId, accessToken, params);
    return { containerId, needsProcessing: false };
  }

  if (postType === 'photo') {
    const containerId = await createContainer(version, threadsUserId, accessToken, withCommon({ media_type: 'IMAGE', image_url: urls[0] }));
    return { containerId, needsProcessing: false };
  }

  if (postType === 'video') {
    const containerId = await createContainer(version, threadsUserId, accessToken, withCommon({ media_type: 'VIDEO', video_url: urls[0] }));
    return { containerId, needsProcessing: true };
  }

  if (postType === 'carousel') {
    // Create each child container, then the parent
    const childIds: string[] = [];
    for (let i = 0; i < urls.length; i++) {
      const filePath = files[i];
      const childParams: Record<string, string> = { is_carousel_item: 'true' };
      if (isVideoPath(filePath)) {
        childParams.media_type = 'VIDEO';
        childParams.video_url = urls[i];
      } else {
        childParams.media_type = 'IMAGE';
        childParams.image_url = urls[i];
      }
      childIds.push(await createContainer(version, threadsUserId, accessToken, childParams));
    }
    publication.threadsChildContainerIds = childIds;
    const containerId = await createContainer(version, threadsUserId, accessToken, withCommon({
      media_type: 'CAROUSEL',
      children: childIds.join(','),
    }));
    return { containerId, needsProcessing: true };
  }

  throw Object.assign(new Error(`Unsupported Threads post type: ${postType}`), { code: 'validation', retryable: false });
}

async function finishPublish(version: string, threadsUserId: string, accessToken: string, publication: ISocialMediaPublication, containerId: string): Promise<void> {
  const mediaId = await publishContainer(version, threadsUserId, accessToken, containerId);
  publication.threadsMediaId = mediaId;
  publication.platformPostId = mediaId;
  const permalink = await fetchPermalink(version, mediaId, accessToken);
  publication.platformUrl = permalink || 'https://www.threads.net/';
  publication.status = 'published';
  publication.publishedAt = new Date();
  publication.workerLockedAt = null as any;
  await publication.save();
}

// ============================================
// MAIN: PUBLISH A QUEUED PUBLICATION
// ============================================

export async function executePublication(publication: ISocialMediaPublication): Promise<void> {
  // Resolve the owning admin's token — the isolation checkpoint.
  const tokenResult = await getFreshToken(publication.companyId, publication.createdBy, publication.accountRef);
  if (tokenResult.error || !tokenResult.accessToken) {
    await failAttempt(publication, 'account_unavailable', tokenResult.error || 'Connected Threads account unavailable', false);
    return;
  }

  const accessToken = tokenResult.accessToken;
  const threadsUserId = publication.threadsUserId || tokenResult.account?.threadsUserId;
  if (!threadsUserId) {
    await failAttempt(publication, 'account_missing', 'This publication has no target Threads account', false);
    return;
  }

  const postType = publication.postType || 'text';
  const needsMedia = ['photo', 'video', 'carousel'].includes(postType);

  if (needsMedia) {
    if (!publication.mediaFilePaths || publication.mediaFilePaths.length === 0) {
      await failAttempt(publication, 'source_file_missing', 'No media attached to this publication', false);
      return;
    }
    if (!publicBaseUrl()) {
      await failAttempt(publication, 'public_url_missing', 'PUBLIC_BASE_URL is not configured — Threads cannot fetch the media. Set a public HTTPS base URL.', false);
      return;
    }
  }

  try {
    const version = await graphVersion();

    // Fail fast on the 250/day publishing limit
    if (await isQuotaExhausted(version, threadsUserId, accessToken)) {
      await failAttempt(publication, 'rate_limited', 'Threads daily publishing limit (250 posts) reached. It resets 24 hours after your earliest post.', true);
      publication.nextAttemptAt = new Date(Date.now() + 60 * 60 * 1000);
      await publication.save();
      return;
    }

    publication.status = 'uploading';
    await publication.save();

    const { containerId, needsProcessing } = await buildPublishableContainer(version, threadsUserId, accessToken, publication);
    publication.threadsContainerId = containerId;
    await publication.save();

    // Inline readiness poll — publishes text/images instantly, defers long video
    // processing to the worker's status sync.
    if (needsProcessing) {
      let statusValue = 'IN_PROGRESS';
      for (let i = 0; i < INLINE_POLL_TRIES; i++) {
        statusValue = await getContainerStatus(version, containerId, accessToken);
        if (statusValue !== 'IN_PROGRESS') break;
        await new Promise((r) => setTimeout(r, INLINE_POLL_DELAY_MS));
      }
      if (statusValue === 'ERROR' || statusValue === 'EXPIRED') {
        await failAttempt(publication, 'container_failed', `Threads could not process the media (${statusValue}). Check the media meets Threads' format/aspect/duration rules.`, false);
        return;
      }
      if (statusValue !== 'FINISHED') {
        // Still processing — hand off to the worker's syncPublicationStatus
        publication.status = 'processing';
        publication.workerLockedAt = null as any;
        await publication.save();
        return;
      }
    }

    await finishPublish(version, threadsUserId, accessToken, publication, containerId);
  } catch (error: any) {
    const code = error.code || 'threads_api_error';
    const retryable = error.retryable === true;
    await failAttempt(publication, code, error.message || 'Publish failed', retryable);
  }
}

// ============================================
// STATUS POLLING (video/carousel processing)
// ============================================

export async function syncPublicationStatus(publication: ISocialMediaPublication): Promise<void> {
  if (publication.status !== 'processing' || !publication.threadsContainerId) return;

  const tokenResult = await getFreshToken(publication.companyId, publication.createdBy, publication.accountRef);
  if (tokenResult.error || !tokenResult.accessToken) return;

  const version = await graphVersion();
  const accessToken = tokenResult.accessToken;
  const threadsUserId = publication.threadsUserId || tokenResult.account?.threadsUserId;
  if (!threadsUserId) return;

  try {
    const statusValue = await getContainerStatus(version, publication.threadsContainerId, accessToken);

    if (statusValue === 'FINISHED') {
      await finishPublish(version, threadsUserId, accessToken, publication, publication.threadsContainerId);
      return;
    }
    if (statusValue === 'ERROR' || statusValue === 'EXPIRED') {
      recordError(publication, 'container_failed', `Threads media processing ${statusValue}`);
      publication.status = 'failed';
      await publication.save();
    }
    // IN_PROGRESS → leave for the next tick
  } catch (error) {
    console.error('Threads status sync error:', error);
  }
}
