import { Op, UniqueConstraintError, type Transaction } from "sequelize";
import { initDb } from "@/lib/db";
import type { DomesticQuestionVersion } from "@/lib/db/models/domestic-question-version";
import type { QueueReason } from "@/types/participant";
import {
  FEED_WINDOW,
  SKIP_COOLDOWN_DAYS,
  evaluateCondition,
} from "@/lib/domestic/rules";

const ACTIVE_STATUSES = ["queued", "served"] as const;

export function queueActiveKey(profileId: string, questionId: string) {
  return `${profileId}:${questionId}`;
}

/** Fetch (creating on first use) and row-lock the participant's feed state. */
export async function lockDomesticState(
  participantProfileId: string,
  transaction: Transaction
) {
  const { ParticipantDomesticState } = initDb();
  await ParticipantDomesticState.findOrCreate({
    where: { participantProfileId },
    defaults: { participantProfileId },
    transaction,
  });
  return ParticipantDomesticState.findOne({
    where: { participantProfileId },
    transaction,
    lock: transaction.LOCK.UPDATE,
  }).then((s) => s!);
}

async function activeQueueItems(
  participantProfileId: string,
  transaction: Transaction
) {
  const { ParticipantQuestionQueue } = initDb();
  return ParticipantQuestionQueue.findAll({
    where: { participantProfileId, status: { [Op.in]: [...ACTIVE_STATUSES] } },
    order: [
      ["position", "ASC"],
      ["createdAt", "ASC"],
    ],
    transaction,
  });
}

async function answeredQuestionIds(
  participantProfileId: string,
  transaction: Transaction
) {
  const { DomesticAnswer } = initDb();
  const rows = await DomesticAnswer.findAll({
    where: { participantProfileId },
    attributes: ["domesticQuestionId"],
    group: ["domesticQuestionId"],
    transaction,
  });
  return rows.map((r) => r.domesticQuestionId);
}

async function recentlySkippedQuestionIds(
  participantProfileId: string,
  transaction: Transaction
) {
  const { ParticipantQuestionQueue } = initDb();
  const since = new Date(Date.now() - SKIP_COOLDOWN_DAYS * 86_400_000);
  const rows = await ParticipantQuestionQueue.findAll({
    where: {
      participantProfileId,
      status: "skipped",
      updatedAt: { [Op.gte]: since },
    },
    attributes: ["domesticQuestionId"],
    transaction,
  });
  return rows.map((r) => r.domesticQuestionId);
}

/** Policy ids the participant currently has granted. */
async function grantedPolicyIds(
  participantProfileId: string,
  transaction: Transaction
) {
  const { ParticipantConsentRecord } = initDb();
  const records = await ParticipantConsentRecord.findAll({
    where: { participantProfileId },
    order: [["createdAt", "DESC"]],
    transaction,
  });
  const latest = new Map<string, string>();
  for (const r of records) {
    if (!latest.has(r.consentPolicyId)) latest.set(r.consentPolicyId, r.status);
  }
  return new Set(
    Array.from(latest.entries())
      .filter(([, status]) => status === "granted")
      .map(([id]) => id)
  );
}

/**
 * Queue a question's current version for a participant. Returns false when it
 * is unavailable, already answered/active, or suppressed for this participant.
 */
export async function enqueueQuestion(
  participantProfileId: string,
  questionId: string,
  opts: {
    reason: QueueReason;
    position: number;
    sourceEdgeId?: string | null;
    allowNonEntry?: boolean;
    suppressed: Set<string>;
    answered: Set<string>;
  },
  transaction: Transaction
): Promise<boolean> {
  if (opts.suppressed.has(questionId) || opts.answered.has(questionId)) {
    return false;
  }

  const { DomesticQuestion, ParticipantQuestionQueue } = initDb();
  const question = await DomesticQuestion.findByPk(questionId, { transaction });
  if (
    !question ||
    question.status !== "published" ||
    !question.currentVersionId ||
    (!question.isEntry && !opts.allowNonEntry)
  ) {
    return false;
  }

  try {
    await ParticipantQuestionQueue.create(
      {
        participantProfileId,
        domesticQuestionId: question.id,
        questionVersionId: question.currentVersionId,
        position: opts.position,
        reason: opts.reason,
        status: "queued",
        activeKey: queueActiveKey(participantProfileId, question.id),
        sourceEdgeId: opts.sourceEdgeId ?? null,
        availableAt: new Date(),
      },
      { transaction }
    );
    return true;
  } catch (err) {
    if (err instanceof UniqueConstraintError) return false;
    throw err;
  }
}

/** Top up the queue to the presentation window using weighted rotation. */
export async function refillQueue(
  participantProfileId: string,
  transaction: Transaction,
  suppressedIds?: string[]
) {
  const { DomesticQuestion, DomesticQuestionVersion, DomesticCategory, ParticipantQuestionQueue } =
    initDb();

  const active = await activeQueueItems(participantProfileId, transaction);
  const missing = FEED_WINDOW - active.length;
  if (missing <= 0) return 0;

  const [answered, skipped, granted] = await Promise.all([
    answeredQuestionIds(participantProfileId, transaction),
    recentlySkippedQuestionIds(participantProfileId, transaction),
    grantedPolicyIds(participantProfileId, transaction),
  ]);

  const excluded = new Set<string>([
    ...answered,
    ...skipped,
    ...(suppressedIds ?? []),
    ...active.map((a) => a.domesticQuestionId),
  ]);

  const candidates = await DomesticQuestion.findAll({
    where: {
      status: "published",
      isEntry: true,
      currentVersionId: { [Op.ne]: null },
      ...(excluded.size ? { id: { [Op.notIn]: Array.from(excluded) } } : {}),
    },
    include: [
      {
        model: DomesticCategory,
        as: "category",
        where: { isActive: true },
        attributes: ["id", "sortOrder"],
      },
      {
        model: DomesticQuestionVersion,
        as: "currentVersion",
        attributes: ["id", "consentPolicyId"],
      },
    ],
    order: [
      ["rotationWeight", "DESC"],
      ["code", "ASC"],
    ],
    limit: missing * 3,
    transaction,
  });

  const everQueued = await ParticipantQuestionQueue.count({
    where: { participantProfileId },
    transaction,
  });
  const reason: QueueReason = everQueued === 0 ? "seed" : "rotation";

  let position = active.length
    ? Math.max(...active.map((a) => a.position)) + 1
    : 0;
  let added = 0;

  for (const q of candidates) {
    if (added >= missing) break;
    const version = (q as unknown as { currentVersion?: DomesticQuestionVersion })
      .currentVersion;
    if (version?.consentPolicyId && !granted.has(version.consentPolicyId)) {
      continue;
    }
    const ok = await enqueueQuestion(
      participantProfileId,
      q.id,
      {
        reason,
        position,
        suppressed: new Set(suppressedIds ?? []),
        answered: new Set(answered),
      },
      transaction
    );
    if (ok) {
      position += 1;
      added += 1;
    }
  }

  return added;
}

/**
 * Evaluate a version's outgoing edges in priority order. Returns the ids of
 * edges that fired (stored on the answer for auditability).
 */
export async function applyEdges(
  participantProfileId: string,
  version: DomesticQuestionVersion,
  value: unknown,
  suppressed: Set<string>,
  transaction: Transaction
) {
  const { DomesticQuestionEdge, ParticipantQuestionQueue } = initDb();
  const now = new Date();
  const edges = await DomesticQuestionEdge.findAll({
    where: {
      fromQuestionVersionId: version.id,
      effectiveFrom: { [Op.lte]: now },
      [Op.or]: [{ effectiveTo: null }, { effectiveTo: { [Op.gt]: now } }],
    },
    order: [
      ["priority", "ASC"],
      ["createdAt", "ASC"],
    ],
    transaction,
  });

  const applied: string[] = [];
  const answered = new Set(
    await answeredQuestionIds(participantProfileId, transaction)
  );
  answered.add(version.domesticQuestionId);

  for (const edge of edges) {
    if (!evaluateCondition(edge.conditionType, edge.conditionConfig, value)) {
      continue;
    }
    applied.push(edge.id);

    const active = await activeQueueItems(participantProfileId, transaction);
    const front = active.length ? Math.min(...active.map((a) => a.position)) - 1 : 0;
    const back = active.length ? Math.max(...active.map((a) => a.position)) + 1 : 0;
    const existing = edge.toQuestionId
      ? active.find((a) => a.domesticQuestionId === edge.toQuestionId)
      : undefined;

    if (edge.edgeType === "unlock" && edge.toQuestionId) {
      if (existing) {
        await existing.update({ position: front }, { transaction });
      } else {
        await enqueueQuestion(
          participantProfileId,
          edge.toQuestionId,
          {
            reason: "branch",
            position: front,
            sourceEdgeId: edge.id,
            allowNonEntry: true,
            suppressed,
            answered,
          },
          transaction
        );
      }
    } else if (edge.edgeType === "boost" && edge.toQuestionId) {
      if (existing) {
        await existing.update({ position: front }, { transaction });
      } else {
        await enqueueQuestion(
          participantProfileId,
          edge.toQuestionId,
          {
            reason: "branch",
            position: back,
            sourceEdgeId: edge.id,
            allowNonEntry: true,
            suppressed,
            answered,
          },
          transaction
        );
      }
    } else if (edge.edgeType === "suppress" || edge.edgeType === "terminate") {
      if (edge.toQuestionId) {
        suppressed.add(edge.toQuestionId);
        await ParticipantQuestionQueue.update(
          { status: "suppressed", activeKey: null },
          {
            where: {
              participantProfileId,
              domesticQuestionId: edge.toQuestionId,
              status: { [Op.in]: [...ACTIVE_STATUSES] },
            },
            transaction,
          }
        );
      }
      if (edge.edgeType === "terminate") break;
    }
  }

  return applied;
}

/**
 * Expire queued items whose question is no longer published, and move
 * unanswered items onto the question's latest published version.
 */
async function reconcileActiveItems(
  participantProfileId: string,
  transaction: Transaction
) {
  const { DomesticQuestion } = initDb();
  const active = await activeQueueItems(participantProfileId, transaction);
  if (!active.length) return;

  const questions = await DomesticQuestion.findAll({
    where: { id: { [Op.in]: active.map((a) => a.domesticQuestionId) } },
    attributes: ["id", "status", "currentVersionId"],
    transaction,
  });
  const byId = new Map(questions.map((q) => [q.id, q]));

  for (const item of active) {
    const q = byId.get(item.domesticQuestionId);
    if (!q || q.status !== "published" || !q.currentVersionId) {
      await item.update({ status: "expired", activeKey: null }, { transaction });
    } else if (q.currentVersionId !== item.questionVersionId) {
      await item.update({ questionVersionId: q.currentVersionId }, { transaction });
    }
  }
}

/** Ordered presentation window (refills first). */
export async function loadFeedWindow(
  participantProfileId: string,
  transaction: Transaction
) {
  const { ParticipantQuestionQueue, DomesticQuestion, DomesticQuestionVersion, DomesticCategory } =
    initDb();

  const state = await lockDomesticState(participantProfileId, transaction);
  await reconcileActiveItems(participantProfileId, transaction);
  await refillQueue(participantProfileId, transaction, state.suppressedQuestionIds ?? []);

  const items = await ParticipantQuestionQueue.findAll({
    where: { participantProfileId, status: { [Op.in]: [...ACTIVE_STATUSES] } },
    include: [
      { model: DomesticQuestionVersion, as: "version" },
      {
        model: DomesticQuestion,
        as: "question",
        attributes: ["id", "code", "status", "sensitivity"],
        include: [
          { model: DomesticCategory, as: "category", attributes: ["name"] },
        ],
      },
    ],
    order: [
      ["position", "ASC"],
      ["createdAt", "ASC"],
    ],
    limit: FEED_WINDOW,
    transaction,
  });

  const first = items[0];
  if (first && first.status === "queued") {
    const now = new Date();
    await first.update({ status: "served", servedAt: now }, { transaction });
    await state.update({ lastServedAt: now }, { transaction });
  }

  return { state, items };
}
