import { runNextSequenceGeneration } from "@/lib/generation/run-generation";
import { AutoPostJobLogger } from "@/lib/logging/auto-post-logger";
import { ensureFreshAccountTokens } from "@/lib/social/ensure-publish-tokens";
import { getNextQueuedPostForPublish } from "@/lib/storage/generated-posts";
import { getUserProjects, listAllUserIds } from "@/lib/storage/projects";
import { mergeAccountsForPublish } from "@/lib/storage/social-accounts";
import { resolvePublishReadyPlatforms, resolvePublishTargetsForPost } from "@/lib/social/publish-platforms";
import type { Project } from "@/types/workflow";
import { withAutoPostLock } from "./auto-post-lock";
import { publishPostAtSlot } from "./publish-at-slot";
import { writeSchedulerHeartbeat, type SchedulerTickSource } from "./scheduler-heartbeat";
import { getActiveScheduleSlots } from "./slot-check";
import { resolveScheduleSeed, buildPlannedScheduleView } from "./resolve-schedule";
import { tryClaimSlotFire, attachPostToSlotFire } from "./slot-fire-state";

export interface ScheduledPublishResult {
  projectId: string;
  projectName: string;
  slot: string;
  status: "generated_published" | "published_existing" | "skipped" | "failed";
  postId?: string;
  message: string;
  jobId?: string;
}

export interface ScheduledPublishRunSummary {
  jobId: string;
  checkedAt: string;
  projectsChecked: number;
  results: ScheduledPublishResult[];
  skippedReason?: string;
}

function automationEnabled(
  automation: Record<string, boolean> | undefined,
  ruleId: string,
  defaultOn: boolean,
): boolean {
  return automation?.[ruleId] ?? defaultOn;
}

async function processProjectSlot(input: {
  userId: string;
  project: Project;
  slotLabel: string;
  scheduleSeed: number;
  postsPerDay: 1 | 2;
  automation: Record<string, boolean>;
  now: Date;
  logger: AutoPostJobLogger;
}): Promise<ScheduledPublishResult> {
  const { userId, project, slotLabel, scheduleSeed, postsPerDay, automation, now, logger } =
    input;
  const base = {
    projectId: project.id,
    projectName: project.name,
    slot: slotLabel,
    jobId: logger.jobId,
  };

  await logger.info("project_loaded", `Processing project "${project.name}" for ${slotLabel} slot.`, {
    projectId: project.id,
    status: "processing",
    details: { slot: slotLabel, timezone: project.timezone },
  });

  const snapshot = await getUserProjects(userId);
  const data = snapshot?.dataByProjectId[project.id];

  if (!data?.context?.saved) {
    const message = "Posting context not saved — skipping scheduled fire.";
    await logger.warn("validation_failed", message, {
      projectId: project.id,
      status: "skipped",
      details: { reason: "context_not_saved", slot: slotLabel },
    });
    return { ...base, status: "skipped", message };
  }

  await logger.info("schedule_validated", "Schedule and context validated.", {
    projectId: project.id,
    status: "validated",
  });

  const serverAccounts = await ensureFreshAccountTokens(userId, project.id);
  const mergedAccounts = mergeAccountsForPublish(data.accounts, serverAccounts);
  const connectedPlatforms = resolvePublishReadyPlatforms(mergedAccounts);

  if (!connectedPlatforms.length) {
    const message = `No connected publish-ready accounts for ${project.name}. Connect LinkedIn and Facebook in Connected Accounts.`;
    await logger.warn("provider_validation_failed", message, {
      projectId: project.id,
      status: "skipped",
      details: { slot: slotLabel, connectedPlatforms: [] },
    });
    return { ...base, status: "skipped", message };
  }

  await logger.info("providers_validated", `Providers ready: ${connectedPlatforms.join(", ")}.`, {
    projectId: project.id,
    status: "providers_ready",
    details: { providers: connectedPlatforms },
  });

  const queuedPost = await getNextQueuedPostForPublish(project.id, project.name);

  if (queuedPost) {
    if (queuedPost.status === "published" || queuedPost.platformPermalinks) {
      const existing = queuedPost.platformPermalinks ?? {};
      if (Object.keys(existing).length > 0) {
        await logger.info("proposal_already_published", `Queued post already published — skipping.`, {
          projectId: project.id,
          opportunityId: queuedPost.id,
          status: "skipped",
          details: { platforms: Object.keys(existing) },
        });
        return {
          ...base,
          status: "skipped",
          postId: queuedPost.id,
          message: "Post already published — duplicate skipped.",
        };
      }
    }

    await logger.info("proposal_found", `Queued post found: "${queuedPost.title}".`, {
      projectId: project.id,
      opportunityId: queuedPost.id,
      status: "queued",
      details: {
        sequencePostId: queuedPost.sequencePostId,
        postStatus: queuedPost.status,
        score: queuedPost.engagementScore,
      },
    });

    const userApproved = queuedPost.status === "approved";
    const requireHighScore =
      !userApproved && automationEnabled(automation, "high-score", false);
    const minScore = requireHighScore ? 80 : 0;
    if (minScore > 0 && (queuedPost.engagementScore ?? 0) < minScore) {
      const message = `Queued post score ${queuedPost.engagementScore ?? 0} below ${minScore} — not auto-published.`;
      await logger.warn("eligibility_failed", message, {
        projectId: project.id,
        opportunityId: queuedPost.id,
        status: "skipped",
        details: { slot: slotLabel, score: queuedPost.engagementScore, minScore },
      });
      return { ...base, status: "skipped", postId: queuedPost.id, message };
    }

    await logger.info("posting_request_initiated", "Publishing queued post to providers.", {
      projectId: project.id,
      opportunityId: queuedPost.id,
      status: "publishing",
      details: {
        providers: resolvePublishTargetsForPost(mergedAccounts, queuedPost),
      },
    });

    const result = await publishPostAtSlot({
      post: queuedPost,
      project,
      accounts: mergedAccounts,
      slotLabel,
      postsPerDay,
      scheduleSeed,
      now,
      source: "queued",
      logger,
    });

    if (!result.ok) {
      await logger.error("posting_failed", result.message, {
        projectId: project.id,
        opportunityId: queuedPost.id,
        status: "failed",
        details: { slot: slotLabel },
      });
      return {
        ...base,
        status: "failed",
        postId: queuedPost.id,
        message: result.message,
      };
    }

    await logger.success("proposal_posted", result.message, {
      projectId: project.id,
      opportunityId: queuedPost.id,
      status: "published",
      details: { platforms: result.platforms, slot: slotLabel },
    });

    return {
      ...base,
      status: "published_existing",
      postId: queuedPost.id,
      message: result.message,
    };
  }

  let post;
  try {
    if (automationEnabled(automation, "loop-sequence", true)) {
      await logger.info("proposal_generate_started", "No queued post — generating next in sequence.", {
        projectId: project.id,
        status: "generating",
      });

      post = await runNextSequenceGeneration(
        {
          projectId: project.id,
          project,
          brand: { ...data.brand, companyName: data.brand.companyName || project.name },
          context: data.context,
          connectedPlatforms,
          schedule: data.schedule,
          automation: data.automation,
          accounts: mergeAccountsForPublish(data.accounts, serverAccounts),
        },
        { userId },
      );

      await logger.info("proposal_generated", `Generated post: "${post.title}".`, {
        projectId: project.id,
        opportunityId: post.id,
        status: "generated",
        details: { score: post.engagementScore },
      });
    } else {
      const message =
        "No post queued for this slot. Enable Loop Post Sequence in Automation, or generate content in Live Generation.";
      await logger.warn("no_proposal_queued", message, {
        projectId: project.id,
        status: "skipped",
        details: { slot: slotLabel },
      });
      return { ...base, status: "skipped", message };
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : "Generation failed";
    await logger.error("proposal_generate_failed", message, {
      projectId: project.id,
      status: "failed",
      error,
      details: { slot: slotLabel },
    });
    return { ...base, status: "failed", message };
  }

  const minScore = automationEnabled(automation, "high-score", false) ? 80 : 0;
  if (minScore > 0 && (post.engagementScore ?? 0) < minScore) {
    const message = `Engagement score ${post.engagementScore ?? 0} below ${minScore} — post generated but not auto-published.`;
    await logger.warn("eligibility_failed", message, {
      projectId: project.id,
      opportunityId: post.id,
      status: "skipped",
      details: { slot: slotLabel, score: post.engagementScore },
    });
    return { ...base, status: "skipped", postId: post.id, message };
  }

  const publishResult = await publishPostAtSlot({
    post,
    project,
    accounts: mergedAccounts,
    slotLabel,
    postsPerDay,
    scheduleSeed,
    now,
    source: "generated",
    logger,
  });

  if (!publishResult.ok) {
    await logger.error("posting_failed", publishResult.message, {
      projectId: project.id,
      opportunityId: post.id,
      status: "failed",
      error: publishResult.message,
    });
    return {
      ...base,
      status: "failed",
      postId: post.id,
      message: publishResult.message,
    };
  }

  await logger.success("proposal_posted", publishResult.message, {
    projectId: project.id,
    opportunityId: post.id,
    status: "published",
  });

  return {
    ...base,
    status: "generated_published",
    postId: post.id,
    message: publishResult.message,
  };
}

export interface ScheduledPublishOptions {
  /** Manual "Publish now" — publish queued posts without waiting for morning/evening slot. */
  bypassSlotWindow?: boolean;
}

async function executeScheduledPublish(
  logger: AutoPostJobLogger,
  now: Date,
  options?: ScheduledPublishOptions,
): Promise<ScheduledPublishRunSummary> {
  const userIds = await listAllUserIds();
  const results: ScheduledPublishResult[] = [];
  let projectsChecked = 0;

  if (!process.env.CRON_SECRET?.trim()) {
    await logger.error("auto_post_stopped", "CRON_SECRET is not set — scheduler cannot run.", {
      status: "misconfigured",
    });
  }

  if (!userIds.length) {
    await logger.warn("auto_post_no_users", "No server project data in data/users/.", {
      status: "no_data",
    });
  } else {
    await logger.info("users_loaded", `Found ${userIds.length} user workspace(s).`, {
      status: "running",
      details: { userIds },
    });
  }

  for (const userId of userIds) {
    const snapshot = await getUserProjects(userId);
    if (!snapshot) continue;

    for (const project of snapshot.projects) {
      if (project.status !== "active") {
        await logger.info("auto_post_paused", `Project "${project.name}" is paused — skipping.`, {
          projectId: project.id,
          status: "paused",
        });
        continue;
      }

      const data = snapshot.dataByProjectId[project.id];
      if (!data?.schedule?.saved) {
        await logger.info("schedule_not_saved", `Project "${project.name}" — schedule not saved.`, {
          projectId: project.id,
          status: "skipped",
        });
        continue;
      }

      if (!automationEnabled(data.automation, "smart-timing", true)) {
        await logger.info("smart_timing_off", `Project "${project.name}" — smart timing disabled.`, {
          projectId: project.id,
          status: "skipped",
        });
        continue;
      }

      projectsChecked += 1;
      const postsPerDay = data.schedule.postsPerDay ?? 2;
      const scheduleSeed = resolveScheduleSeed(project.id, data.schedule.scheduleSeed);

      const activeSlots = getActiveScheduleSlots({
        timezone: project.timezone,
        postsPerDay,
        scheduleSeed,
        now,
      });

      const slotsToRun =
        activeSlots.length > 0
          ? activeSlots
          : options?.bypassSlotWindow
            ? [{ label: "Manual", display: "Manual — publish now", hour: 0, minute: 0 }]
            : [];

      if (!slotsToRun.length) {
      const scheduleView = buildPlannedScheduleView({
        projectId: project.id,
        timezone: project.timezone,
        postsPerDay,
        scheduleSeed,
      });
      await logger.info("outside_schedule_slot", `Project "${project.name}" — outside slot window.`, {
        projectId: project.id,
        status: "waiting",
        details: {
          timezone: project.timezone,
          postsPerDay,
          nextSlots: scheduleView.slotTimes,
          timezoneLabel: scheduleView.timezoneLabel,
          hint: "Use Publish now in Auto-Post Monitor or Quick Publish in Post Review.",
        },
      });
      continue;
    }

      for (const slot of slotsToRun) {
        const isManualBypass = slot.label === "Manual";
        if (!isManualBypass) {
          // Claim BEFORE generate/publish so a concurrent tick cannot double-fire LinkedIn.
          const claimed = await tryClaimSlotFire({
            projectId: project.id,
            slotLabel: slot.label,
            timezone: project.timezone,
            now,
            jobId: logger.jobId,
          });
          if (!claimed) {
            await logger.info("slot_already_fired", `${slot.label} slot already claimed today.`, {
              projectId: project.id,
              status: "skipped",
              details: { slot: slot.label },
            });
            results.push({
              projectId: project.id,
              projectName: project.name,
              slot: slot.label,
              status: "skipped",
              message: `${slot.label} slot already fired today.`,
              jobId: logger.jobId,
            });
            continue;
          }
        }

        await logger.info(
          isManualBypass ? "manual_publish_started" : "slot_processing_started",
          isManualBypass
            ? `Manual publish now for "${project.name}" (bypasses schedule window).`
            : `Firing ${slot.label} slot for "${project.name}".`,
          {
            projectId: project.id,
            status: isManualBypass ? "manual_publish" : "slot_active",
            details: { slot: slot.display },
          },
        );

        const result = await processProjectSlot({
          userId,
          project,
          slotLabel: slot.label,
          scheduleSeed,
          postsPerDay,
          automation: data.automation,
          now,
          logger,
        });

        if (result.postId && !isManualBypass) {
          await attachPostToSlotFire({
            projectId: project.id,
            slotLabel: slot.label,
            timezone: project.timezone,
            postId: result.postId,
            now,
          });
        }

        results.push(result);
      }
    }
  }

  await logger.info("auto_post_tick_completed", `Tick finished — ${results.length} slot action(s).`, {
    status: "completed",
    details: { projectsChecked, resultsCount: results.length },
  });

  return {
    jobId: logger.jobId,
    checkedAt: now.toISOString(),
    projectsChecked,
    results,
  };
}

export async function runScheduledPublish(
  source: SchedulerTickSource = "poller",
  now = new Date(),
  options?: ScheduledPublishOptions,
): Promise<ScheduledPublishRunSummary> {
  const logger = new AutoPostJobLogger();

  await logger.info("auto_post_started", "Scheduled auto-post tick started.", {
    status: "started",
    details: { now: now.toISOString(), source, bypassSlotWindow: options?.bypassSlotWindow ?? false },
  });

  const locked = await withAutoPostLock(logger.jobId, () =>
    executeScheduledPublish(logger, now, options),
  );

  if (!locked.ok) {
    await logger.warn("auto_post_skipped_duplicate", locked.reason, {
      status: "skipped",
    });
    await writeSchedulerHeartbeat(source);
    return {
      jobId: logger.jobId,
      checkedAt: now.toISOString(),
      projectsChecked: 0,
      results: [],
      skippedReason: locked.reason,
    };
  }

  await writeSchedulerHeartbeat(source);
  return locked.result;
}
