"use server";

import { revalidatePath } from "next/cache";
import { Op, UniqueConstraintError, type Transaction } from "sequelize";
import { initDb } from "@/lib/db";
import { requireSuperAdmin } from "@/lib/auth/require-super-admin";
import {
  createDomesticQuestionSchema,
  domesticCategorySchema,
  domesticContentSchema,
  domesticEdgeSchema,
  domesticMetaSchema,
  domesticStatusSchema,
  type CreateDomesticQuestionInput,
  type DomesticContentInput,
  type DomesticEdgeInput,
  type DomesticMetaInput,
} from "@/lib/validation/domestic";
import {
  edgeNeedsTarget,
  normaliseContent,
  validateEdgeCondition,
  validateForPublish,
  wouldCreateCycle,
} from "@/lib/domestic/graph";
import type { ActionResult } from "@/lib/actions/auth";
import type { DomesticConditionConfig } from "@/types/participant";

class AdminDomesticError extends Error {}

function firstIssue(err: { issues: Array<{ message: string }> }) {
  return err.issues[0]?.message ?? "Invalid input";
}

function revalidateDomestic(questionId?: string) {
  revalidatePath("/admin/domestic");
  if (questionId) revalidatePath(`/admin/domestic/questions/${questionId}`);
}

async function audit(
  actorUserId: string,
  action: string,
  metadata: Record<string, unknown>,
  transaction?: Transaction
) {
  const { AuditLog } = initDb();
  await AuditLog.create(
    { actorUserId, workspaceId: null, action, metadata },
    { transaction }
  );
}

async function nextQuestionCode(transaction: Transaction) {
  const { DomesticQuestion } = initDb();
  const rows = await DomesticQuestion.findAll({
    attributes: ["code"],
    transaction,
  });
  const max = rows.reduce((acc, r) => {
    const n = Number(r.code.replace(/^DOM-/, ""));
    return Number.isFinite(n) && n > acc ? n : acc;
  }, 0);
  return `DOM-${String(max + 10).padStart(4, "0")}`;
}

export async function createDomesticCategory(input: {
  name: string;
  description?: string;
}): Promise<ActionResult<{ id: string }>> {
  const { userId } = await requireSuperAdmin();
  const parsed = domesticCategorySchema.safeParse(input);
  if (!parsed.success) return { success: false, error: firstIssue(parsed.error) };

  const { DomesticCategory } = initDb();
  const key = parsed.data.name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .slice(0, 64);

  try {
    const count = await DomesticCategory.count();
    const category = await DomesticCategory.create({
      key,
      name: parsed.data.name,
      description: parsed.data.description,
      sortOrder: count * 10,
    });
    await audit(userId, "domestic.category_created", { key });
    revalidateDomestic();
    return { success: true, data: { id: category.id } };
  } catch (err) {
    if (err instanceof UniqueConstraintError) {
      return { success: false, error: "A category with that name already exists" };
    }
    return { success: false, error: "Failed to create category" };
  }
}

export async function toggleDomesticCategory(
  categoryId: string
): Promise<ActionResult> {
  const { userId } = await requireSuperAdmin();
  const { DomesticCategory } = initDb();
  const category = await DomesticCategory.findByPk(categoryId);
  if (!category) return { success: false, error: "Category not found" };
  await category.update({ isActive: !category.isActive });
  await audit(userId, "domestic.category_toggled", {
    key: category.key,
    isActive: category.isActive,
  });
  revalidateDomestic();
  return { success: true };
}

export async function createDomesticQuestion(
  input: CreateDomesticQuestionInput
): Promise<ActionResult<{ id: string }>> {
  const { userId } = await requireSuperAdmin();
  const parsed = createDomesticQuestionSchema.safeParse(input);
  if (!parsed.success) return { success: false, error: firstIssue(parsed.error) };

  const { sequelize, DomesticQuestion, DomesticQuestionVersion, DomesticCategory } =
    initDb();

  try {
    const id = await sequelize.transaction(async (t) => {
      const category = await DomesticCategory.findByPk(parsed.data.meta.categoryId, {
        transaction: t,
      });
      if (!category) throw new AdminDomesticError("Category not found");

      const code = parsed.data.code ?? (await nextQuestionCode(t));
      const content = normaliseContent(parsed.data.content);

      const question = await DomesticQuestion.create(
        {
          code,
          categoryId: category.id,
          status: "draft",
          sensitivity: parsed.data.meta.sensitivity,
          isEntry: parsed.data.meta.isEntry,
          rotationWeight: parsed.data.meta.rotationWeight,
          createdBy: userId,
        },
        { transaction: t }
      );
      const version = await DomesticQuestionVersion.create(
        {
          domesticQuestionId: question.id,
          versionNumber: 1,
          ...content,
          createdBy: userId,
        },
        { transaction: t }
      );
      await question.update({ currentVersionId: version.id }, { transaction: t });
      await audit(userId, "domestic.question_created", { code }, t);
      return question.id;
    });

    revalidateDomestic();
    return { success: true, data: { id } };
  } catch (err) {
    if (err instanceof AdminDomesticError) return { success: false, error: err.message };
    if (err instanceof UniqueConstraintError) {
      return { success: false, error: "That question code is already used" };
    }
    return { success: false, error: "Failed to create question" };
  }
}

/**
 * Save question content. Unpublished versions are edited in place;
 * published versions are immutable, so a new version is created and edges
 * are carried forward when the question type is unchanged.
 */
export async function saveDomesticQuestionContent(
  questionId: string,
  input: DomesticContentInput
): Promise<ActionResult<{ versionNumber: number; newVersion: boolean }>> {
  const { userId } = await requireSuperAdmin();
  const parsed = domesticContentSchema.safeParse(input);
  if (!parsed.success) return { success: false, error: firstIssue(parsed.error) };

  const { sequelize, DomesticQuestion, DomesticQuestionVersion, DomesticQuestionEdge } =
    initDb();

  try {
    const result = await sequelize.transaction(async (t) => {
      const question = await DomesticQuestion.findByPk(questionId, {
        transaction: t,
        lock: t.LOCK.UPDATE,
      });
      if (!question) throw new AdminDomesticError("Question not found");
      if (question.status === "retired") {
        throw new AdminDomesticError("Retired questions can't be edited");
      }
      // Draft, review, paused, and published are editable; only retired is locked.

      const current = question.currentVersionId
        ? await DomesticQuestionVersion.findByPk(question.currentVersionId, {
            transaction: t,
          })
        : null;
      const content = normaliseContent(parsed.data);

      if (current && current.contentHash === content.contentHash) {
        return { versionNumber: current.versionNumber, newVersion: false };
      }

      if (current && !current.publishedAt) {
        await current.update(content, { transaction: t });
        return { versionNumber: current.versionNumber, newVersion: false };
      }

      const latest = await DomesticQuestionVersion.max<
        number,
        InstanceType<typeof DomesticQuestionVersion>
      >(
        "versionNumber",
        { where: { domesticQuestionId: question.id }, transaction: t }
      );
      const now = new Date();
      const isLive = question.status === "published";
      const version = await DomesticQuestionVersion.create(
        {
          domesticQuestionId: question.id,
          versionNumber: (latest ?? 0) + 1,
          ...content,
          createdBy: userId,
          publishedAt: isLive ? now : null,
        },
        { transaction: t }
      );

      if (current) {
        await current.update({ retiredAt: now }, { transaction: t });
        if (current.type === content.type) {
          const edges = await DomesticQuestionEdge.findAll({
            where: {
              fromQuestionVersionId: current.id,
              [Op.or]: [{ effectiveTo: null }, { effectiveTo: { [Op.gt]: now } }],
            },
            transaction: t,
          });
          for (const e of edges) {
            const err = validateEdgeCondition(
              content.type,
              content.optionsConfig,
              e.conditionType,
              e.conditionConfig
            );
            if (err) continue;
            await DomesticQuestionEdge.create(
              {
                fromQuestionVersionId: version.id,
                conditionType: e.conditionType,
                conditionConfig: e.conditionConfig,
                toQuestionId: e.toQuestionId,
                priority: e.priority,
                edgeType: e.edgeType,
                effectiveFrom: now,
                createdBy: userId,
              },
              { transaction: t }
            );
          }
        }
      }

      await question.update({ currentVersionId: version.id }, { transaction: t });
      await audit(
        userId,
        "domestic.question_versioned",
        { code: question.code, versionNumber: version.versionNumber },
        t
      );
      return { versionNumber: version.versionNumber, newVersion: true };
    });

    revalidateDomestic(questionId);
    return { success: true, data: result };
  } catch (err) {
    if (err instanceof AdminDomesticError) return { success: false, error: err.message };
    return { success: false, error: "Failed to save question" };
  }
}

export async function updateDomesticQuestionMeta(
  questionId: string,
  input: DomesticMetaInput
): Promise<ActionResult> {
  const { userId } = await requireSuperAdmin();
  const parsed = domesticMetaSchema.safeParse(input);
  if (!parsed.success) return { success: false, error: firstIssue(parsed.error) };

  const { DomesticQuestion, DomesticCategory } = initDb();
  const question = await DomesticQuestion.findByPk(questionId);
  if (!question) return { success: false, error: "Question not found" };
  const category = await DomesticCategory.findByPk(parsed.data.categoryId);
  if (!category) return { success: false, error: "Category not found" };

  await question.update(parsed.data);
  await audit(userId, "domestic.question_settings_updated", {
    code: question.code,
    ...parsed.data,
  });
  revalidateDomestic(questionId);
  return { success: true };
}

export async function setDomesticQuestionStatus(input: {
  questionId: string;
  status: "published" | "paused" | "retired";
}): Promise<ActionResult<{ problems?: string[] }>> {
  const { userId } = await requireSuperAdmin();
  const parsed = domesticStatusSchema.safeParse(input);
  if (!parsed.success) return { success: false, error: "Invalid request" };

  const { sequelize, DomesticQuestion, DomesticQuestionVersion } = initDb();

  try {
    await sequelize.transaction(async (t) => {
      const question = await DomesticQuestion.findByPk(parsed.data.questionId, {
        transaction: t,
        lock: t.LOCK.UPDATE,
      });
      if (!question) throw new AdminDomesticError("Question not found");
      if (question.status === "retired") {
        throw new AdminDomesticError("Retired questions can't change status");
      }

      const now = new Date();
      if (parsed.data.status === "published") {
        const problems = await validateForPublish(question.id, t);
        if (problems.length) throw new AdminDomesticError(problems.join(" · "));

        const version = await DomesticQuestionVersion.findByPk(
          question.currentVersionId!,
          { transaction: t }
        );
        if (version && !version.publishedAt) {
          await version.update({ publishedAt: now }, { transaction: t });
        }
        await question.update(
          {
            status: "published",
            publishedAt: question.publishedAt ?? now,
            approvedBy: userId,
          },
          { transaction: t }
        );
      } else {
        await question.update({ status: parsed.data.status }, { transaction: t });
        if (parsed.data.status === "retired" && question.currentVersionId) {
          await DomesticQuestionVersion.update(
            { retiredAt: now },
            { where: { id: question.currentVersionId, retiredAt: null }, transaction: t }
          );
        }
      }

      await audit(
        userId,
        `domestic.question_${parsed.data.status}`,
        { code: question.code },
        t
      );
    });
  } catch (err) {
    if (err instanceof AdminDomesticError) return { success: false, error: err.message };
    return { success: false, error: "Failed to update status" };
  }

  revalidateDomestic(parsed.data.questionId);
  return { success: true };
}

export async function addDomesticEdge(
  input: DomesticEdgeInput
): Promise<ActionResult> {
  const { userId } = await requireSuperAdmin();
  const parsed = domesticEdgeSchema.safeParse(input);
  if (!parsed.success) return { success: false, error: firstIssue(parsed.error) };
  const data = parsed.data;

  const { sequelize, DomesticQuestion, DomesticQuestionVersion, DomesticQuestionEdge } =
    initDb();

  try {
    await sequelize.transaction(async (t) => {
      const question = await DomesticQuestion.findByPk(data.questionId, {
        transaction: t,
      });
      if (!question?.currentVersionId) {
        throw new AdminDomesticError("Question not found");
      }
      const version = await DomesticQuestionVersion.findByPk(
        question.currentVersionId,
        { transaction: t }
      );
      if (!version) throw new AdminDomesticError("Question has no content");

      if (edgeNeedsTarget(data.edgeType) && !data.toQuestionId) {
        throw new AdminDomesticError("Choose the question this rule affects");
      }

      const conditionConfig: DomesticConditionConfig | null =
        data.conditionType === "always"
          ? null
          : data.conditionType === "any_of"
            ? { values: data.conditionValues ?? [] }
            : { value: data.conditionValue };

      const conditionError = validateEdgeCondition(
        version.type,
        version.optionsConfig,
        data.conditionType,
        conditionConfig
      );
      if (conditionError) throw new AdminDomesticError(conditionError);

      if (data.toQuestionId) {
        const target = await DomesticQuestion.findByPk(data.toQuestionId, {
          transaction: t,
        });
        if (!target) throw new AdminDomesticError("Target question not found");
        if (target.status === "retired") {
          throw new AdminDomesticError("Target question is retired");
        }
        if (
          (data.edgeType === "unlock" || data.edgeType === "boost") &&
          (await wouldCreateCycle(question.id, target.id, t))
        ) {
          throw new AdminDomesticError(
            "This rule would create a loop — the target already leads back here"
          );
        }
      }

      const clash = await DomesticQuestionEdge.findOne({
        where: {
          fromQuestionVersionId: version.id,
          priority: data.priority,
          effectiveTo: null,
        },
        transaction: t,
      });
      if (clash) {
        throw new AdminDomesticError(
          `Priority ${data.priority} is already used by another rule`
        );
      }

      await DomesticQuestionEdge.create(
        {
          fromQuestionVersionId: version.id,
          conditionType: data.conditionType,
          conditionConfig,
          toQuestionId: data.toQuestionId ?? null,
          priority: data.priority,
          edgeType: data.edgeType,
          effectiveFrom: new Date(),
          createdBy: userId,
        },
        { transaction: t }
      );
      await audit(
        userId,
        "domestic.edge_added",
        { code: question.code, edgeType: data.edgeType, conditionType: data.conditionType },
        t
      );
    });
  } catch (err) {
    if (err instanceof AdminDomesticError) return { success: false, error: err.message };
    return { success: false, error: "Failed to add branch rule" };
  }

  revalidateDomestic(data.questionId);
  return { success: true };
}

/** Edges are end-dated rather than deleted so past evaluations stay auditable. */
export async function removeDomesticEdge(edgeId: string): Promise<ActionResult> {
  const { userId } = await requireSuperAdmin();
  const { DomesticQuestionEdge, DomesticQuestionVersion } = initDb();

  const edge = await DomesticQuestionEdge.findByPk(edgeId);
  if (!edge || edge.effectiveTo) return { success: false, error: "Rule not found" };

  await edge.update({ effectiveTo: new Date() });
  const version = await DomesticQuestionVersion.findByPk(edge.fromQuestionVersionId);
  await audit(userId, "domestic.edge_removed", { edgeId });

  revalidateDomestic(version?.domesticQuestionId);
  return { success: true };
}
