/**
 * Social Publish Worker
 *
 * Lightweight persistent scheduler for social media publications.
 * The SocialMediaPublication collection is the durable queue — this worker
 * ticks every 60 seconds and:
 *  1. Picks up `queued` publications (respecting nextAttemptAt backoff)
 *     and executes the upload.
 *  2. Re-claims stale `uploading` jobs (e.g. after a server restart)
 *     — the resumable session lets the upload continue where it stopped.
 *  3. Polls `processing`/`scheduled` publications to advance them to
 *     `published` (or surface platform-side failures).
 *
 * Note: scheduled go-live itself is handled natively by the platform (YouTube
 * publishAt on a private video; Facebook scheduled_publish_time), so a downed
 * worker can delay uploads/status updates but never a native scheduled publish.
 *
 * Platform dispatch: each publication carries a `platform`. YouTube is the
 * default path (unchanged); Facebook publications are routed to the Facebook
 * publisher. Adding a new platform means adding a case here — nothing else in
 * the worker changes.
 */

import { executePublication as executeYouTube, syncPublicationStatus as syncYouTube } from './youtubePublisher';
import { executePublication as executeFacebook, syncPublicationStatus as syncFacebook } from '../facebook/facebookPublisher';
import { executePublication as executeInstagram, syncPublicationStatus as syncInstagram } from '../instagram/instagramPublisher';
import { executePublication as executeLinkedIn, syncPublicationStatus as syncLinkedIn } from '../linkedin/linkedinPublisher';
import { executePublication as executeTwitter, syncPublicationStatus as syncTwitter } from '../twitter/twitterPublisher';
import { executePublication as executeThreads, syncPublicationStatus as syncThreads } from '../threads/threadsPublisher';
import { executePublication as executePinterest, syncPublicationStatus as syncPinterest } from '../pinterest/pinterestPublisher';
import { executePublication as executeBlogger, syncPublicationStatus as syncBlogger } from '../blogger/bloggerPublisher';
import { executePublication as executeFacebookN8n, syncPublicationStatus as syncFacebookN8n } from '../facebook-n8n/facebookN8nPublisher';
import { executePublication as executeInstagramN8n, syncPublicationStatus as syncInstagramN8n } from '../instagram-n8n/instagramN8nPublisher';
import { executePublication as executeThreadsN8n, syncPublicationStatus as syncThreadsN8n } from '../threads-n8n/threadsN8nPublisher';
import { notificationService } from '../notificationService';

/**
 * Tell the publication's owner when a job reaches a terminal state.
 *
 * Placed in the worker rather than in each publisher on purpose: the seven
 * platform modules all funnel through here, so one hook covers every platform
 * and a new one inherits it for free — the same reasoning as the AI job manager.
 * Only transitions are notified, so a poll that finds the job still processing
 * stays silent.
 */
function notifyTerminalStatus(publication: any, previousStatus: string): void {
  const status = publication.status;
  if (status === previousStatus) return;
  if (status !== 'published' && status !== 'failed') return;

  const platform = String(publication.platform || 'social');
  const label = platform.charAt(0).toUpperCase() + platform.slice(1);
  const published = status === 'published';

  void notificationService.notifyUser(publication.createdBy, {
    type: published ? 'social.publish.completed' : 'social.publish.failed',
    message: published
      ? `Your ${label} post is live.`
      : `Publishing to ${label} failed: ${publication.lastError?.message || 'the platform rejected the post'}.`,
    organizationId: publication.companyId,
    entityType: 'social_publication',
    entityId: String(publication._id),
    actionUrl: '/social-media-os',
    notifyActor: true,
  });

  // An expired or revoked token is not a per-post problem — nothing else will
  // publish until the account is reconnected, so it is called out separately.
  const code = String(publication.lastError?.code || '');
  if (!published && /token|auth|unauthor|revoked|expired/i.test(code)) {
    void notificationService.notifyUser(publication.createdBy, {
      type: 'social.token.expired',
      message: `Your ${label} connection is no longer valid. Reconnect the account to resume publishing.`,
      organizationId: publication.companyId,
      entityType: 'social_account',
      actionUrl: '/social-media-os',
      // One reconnect prompt per platform, however many posts fail behind it.
      groupKey: `social.token.expired:${publication.companyId}:${platform}`,
      notifyActor: true,
    });
  }
}

/** Route a publication to the correct platform executor. */
function executeForPlatform(publication: any): Promise<void> {
  if (publication.platform === 'facebook') return executeFacebook(publication);
  if (publication.platform === 'facebook-n8n') return executeFacebookN8n(publication);
  if (publication.platform === 'instagram-n8n') return executeInstagramN8n(publication);
  if (publication.platform === 'threads-n8n') return executeThreadsN8n(publication);
  if (publication.platform === 'instagram') return executeInstagram(publication);
  if (publication.platform === 'linkedin') return executeLinkedIn(publication);
  if (publication.platform === 'twitter') return executeTwitter(publication);
  if (publication.platform === 'threads') return executeThreads(publication);
  if (publication.platform === 'pinterest') return executePinterest(publication);
  if (publication.platform === 'blogger') return executeBlogger(publication);
  return executeYouTube(publication);
}

/** Route a publication to the correct platform status-sync. */
function syncForPlatform(publication: any): Promise<void> {
  if (publication.platform === 'facebook') return syncFacebook(publication);
  if (publication.platform === 'facebook-n8n') return syncFacebookN8n(publication);
  if (publication.platform === 'instagram-n8n') return syncInstagramN8n(publication);
  if (publication.platform === 'threads-n8n') return syncThreadsN8n(publication);
  if (publication.platform === 'instagram') return syncInstagram(publication);
  if (publication.platform === 'linkedin') return syncLinkedIn(publication);
  if (publication.platform === 'twitter') return syncTwitter(publication);
  if (publication.platform === 'threads') return syncThreads(publication);
  if (publication.platform === 'pinterest') return syncPinterest(publication);
  if (publication.platform === 'blogger') return syncBlogger(publication);
  return syncYouTube(publication);
}

const TICK_INTERVAL_MS = 60 * 1000;
const STALE_LOCK_MS = 10 * 60 * 1000; // re-claim uploads locked for >10 minutes
const MAX_CONCURRENT_UPLOADS = 2;

let workerTimer: ReturnType<typeof setInterval> | null = null;
let tickRunning = false;

async function processQueuedPublications(): Promise<void> {
  const { getModels } = await import('../../models');
  const { SocialMediaPublication } = getModels();

  const now = new Date();
  const staleBefore = new Date(now.getTime() - STALE_LOCK_MS);

  // Queued jobs whose backoff has elapsed, plus stale uploading jobs (crashed
  // mid-upload). Instagram, LinkedIn, Twitter, Threads, and Pinterest have no
  // native scheduling, so their due `scheduled` jobs are executed here.
  //
  // Blogger is EXCLUDED from the scheduled condition because it handles scheduling
  // natively via the Blogger API's publishDate parameter. Once a Blogger publication
  // is in `scheduled` status with a `bloggerPostId`, the post is already scheduled
  // on Blogger and will go live at the specified time. Re-executing would call
  // publishPost() without a publishDate, publishing immediately instead of letting
  // Blogger's native scheduling handle it. Blogger `scheduled` publications are
  // monitored by pollInFlightPublications() which checks if they've gone live.
  const candidates = await SocialMediaPublication.find({
    $or: [
      {
        status: 'queued',
        $and: [
          { $or: [{ nextAttemptAt: null }, { nextAttemptAt: { $lte: now } }] },
          { $or: [{ workerLockedAt: null }, { workerLockedAt: { $lte: staleBefore } }] },
        ],
      },
      { status: 'uploading', workerLockedAt: { $lte: staleBefore } },
      {
        platform: { $in: ['instagram', 'linkedin', 'twitter', 'threads', 'pinterest'] },
        status: 'scheduled',
        publishAt: { $lte: now },
        $or: [{ workerLockedAt: null }, { workerLockedAt: { $lte: staleBefore } }],
      },
    ],
  }).sort({ createdAt: 1 }).limit(MAX_CONCURRENT_UPLOADS);

  for (const publication of candidates) {
    // Claim the job before working on it
    publication.workerLockedAt = new Date();
    await publication.save();

    const statusBeforeExecute = publication.status;
    try {
      await executeForPlatform(publication);
      notifyTerminalStatus(publication, statusBeforeExecute);
    } catch (error) {
      console.error(`Publish worker: unexpected error on publication ${publication._id}:`, error);
      publication.workerLockedAt = null;
      await publication.save().catch(() => undefined);
    }
  }
}

async function pollInFlightPublications(): Promise<void> {
  const { getModels } = await import('../../models');
  const { SocialMediaPublication } = getModels();

  // Poll any processing/scheduled job that has a platform id set. YouTube stores
  // it in platformVideoId; Facebook in platformPostId. Threads video/carousel
  // jobs are still processing (no media id yet) but carry a container id, so
  // match threadsContainerId too — otherwise they would never advance.
  const inFlight = await SocialMediaPublication.find({
    status: { $in: ['processing', 'scheduled'] },
    $or: [
      { platformVideoId: { $ne: null } },
      { platformPostId: { $ne: null } },
      { threadsContainerId: { $ne: null } },
      { pinterestMediaId: { $ne: null } },
    ],
  }).limit(50);

  for (const publication of inFlight) {
    const statusBeforeSync = publication.status;
    try {
      await syncForPlatform(publication);
      notifyTerminalStatus(publication, statusBeforeSync);
    } catch (error) {
      console.error(`Publish worker: status sync failed for publication ${publication._id}:`, error);    }
  }
}

async function tick(): Promise<void> {
  if (tickRunning) return; // never overlap ticks
  tickRunning = true;
  try {
    await processQueuedPublications();
    await pollInFlightPublications();
  } catch (error) {
    console.error('Publish worker tick error:', error);
  } finally {
    tickRunning = false;
  }
}

export function startPublishWorker(): void {
  if (workerTimer) return;
  workerTimer = setInterval(tick, TICK_INTERVAL_MS);
  // First pass shortly after boot so restarts pick up pending work quickly
  setTimeout(tick, 10 * 1000);
  console.log('📤 Social publish worker started (60s tick)');
}

export function stopPublishWorker(): void {
  if (workerTimer) {
    clearInterval(workerTimer);
    workerTimer = null;
  }
}
