"use server";

import { revalidatePath } from "next/cache";
import { Op, type Transaction } from "sequelize";
import { initDb } from "@/lib/db";
import { requireClientWorkspace } from "@/lib/auth/require-auth";
import {
  createElectionSchema,
  updateElectionSchema,
  updateElectionScheduleSchema,
  electionIdSchema,
  certifyElectionSchema,
  setElectionTestModeSchema,
  setElectionOpenEnrollmentSchema,
} from "@/lib/validation/election";
import { electionMutationBlockedReason } from "@/lib/auth/election-access";
import { getScopedElection } from "@/lib/queries/election-queries";
import { generateUniqueElectionSlug } from "@/lib/utils/election-slug";
import { generateVoterRollSnapshot } from "@/lib/elections/generate-voter-roll";
import type { ActionResult } from "@/lib/actions/auth";

function revalidateElectionPaths(electionId?: string, slug?: string) {
  revalidatePath("/app/elections");
  revalidatePath("/app/dashboard");
  revalidatePath("/admin/elections");
  if (electionId) {
    revalidatePath(`/app/elections/${electionId}`);
  }
  if (slug) {
    revalidatePath(`/e/${slug}`);
  }
}

function firstZodError(error: { issues: { message: string }[] }): string {
  return error.issues[0]?.message ?? "Invalid input";
}

export async function createElection(
  input: unknown
): Promise<ActionResult<{ electionId: string }>> {
  const { userId, workspaceId } = await requireClientWorkspace();
  const parsed = createElectionSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const { sequelize, Election, ElectionPosition, Candidate, AuditLog } = initDb();
  const data = parsed.data;

  try {
    const electionId = await sequelize.transaction(async (t: Transaction) => {
      const election = await Election.create(
        {
          workspaceId,
          title: data.title,
          description: data.description ?? null,
          slug: await generateUniqueElectionSlug(data.title),
          status: "draft",
          ballotSecrecy: data.ballotSecrecy,
          votingMethod: data.votingMethod,
          eligibilityModel: data.eligibilityModel,
          requiresAuthorizationRecord: data.requiresAuthorizationRecord,
          recordDate: data.recordDate,
          quorumRequired: data.quorumRequired,
          quorumCount: data.quorumRequired ? data.quorumCount ?? null : null,
          digitalSignatureRequired: data.digitalSignatureRequired,
          resultsVisibility: data.resultsVisibility,
          isTestMode: data.isTestMode,
          openEnrollment:
            data.eligibilityModel === "member" ? data.openEnrollment : false,
          startAt: data.startAt,
          endAt: data.endAt,
          createdBy: userId,
          securityConfig: { verificationMethod: data.verificationMethod },
        },
        { transaction: t }
      );

      for (let index = 0; index < data.positions.length; index++) {
        const positionInput = data.positions[index];
        const seats =
          positionInput.positionType === "multi_seat"
            ? positionInput.seatsAvailable
            : 1;
        const position = await ElectionPosition.create(
          {
            electionId: election.id,
            title: positionInput.title,
            positionType: positionInput.positionType,
            seatsAvailable: seats,
            position: index,
          },
          { transaction: t }
        );

        for (let cIndex = 0; cIndex < positionInput.candidates.length; cIndex++) {
          const candidateInput = positionInput.candidates[cIndex];
          await Candidate.create(
            {
              electionPositionId: position.id,
              name: candidateInput.name,
              biography: candidateInput.biography ?? null,
              goalsStatement: candidateInput.goalsStatement ?? null,
              status: "approved",
              position: cIndex,
            },
            { transaction: t }
          );
        }
      }

      await AuditLog.create(
        {
          actorUserId: userId,
          workspaceId,
          action: "election.created",
          metadata: { electionId: election.id, isTestMode: election.isTestMode },
        },
        { transaction: t }
      );

      return election.id;
    });

    revalidateElectionPaths(electionId);
    return { success: true, data: { electionId } };
  } catch {
    return { success: false, error: "Failed to create election" };
  }
}

export async function updateElection(
  input: unknown
): Promise<ActionResult> {
  const { userId, workspaceId, role } = await requireClientWorkspace();
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const parsed = updateElectionSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const election = await getScopedElection(parsed.data.electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status === "certified") {
    return {
      success: false,
      error: "Certified elections cannot be edited",
    };
  }

  const data = parsed.data;
  const startAt =
    data.startAt === undefined
      ? election.startAt
      : data.startAt
        ? new Date(data.startAt)
        : null;
  const endAt =
    data.endAt === undefined
      ? election.endAt
      : data.endAt
        ? new Date(data.endAt)
        : null;
  const recordDate =
    data.recordDate === undefined
      ? election.recordDate
      : data.recordDate
        ? data.recordDate.slice(0, 10)
        : null;

  if (startAt && Number.isNaN(startAt.getTime())) {
    return { success: false, error: "Invalid start date" };
  }
  if (endAt && Number.isNaN(endAt.getTime())) {
    return { success: false, error: "Invalid end date" };
  }
  if (startAt && endAt && startAt.getTime() > endAt.getTime()) {
    return {
      success: false,
      error: "Close date must be on or after the open date",
    };
  }
  if (recordDate && startAt) {
    const record = new Date(`${recordDate}T00:00:00`);
    if (
      !Number.isNaN(record.getTime()) &&
      record.getTime() > startAt.getTime()
    ) {
      return {
        success: false,
        error: "Record date must be on or before the open date",
      };
    }
  }

  const { sequelize, ElectionPosition, Candidate, CandidateQA, Ballot, AuditLog } =
    initDb();

  try {
    await sequelize.transaction(async (t: Transaction) => {
      if (data.positions !== undefined) {
        const ballotCount = await Ballot.count({
          where: { electionId: election.id },
          transaction: t,
        });
        if (ballotCount > 0) {
          throw new Error("BALLOTS_CAST");
        }

        const existingPositions = await ElectionPosition.findAll({
          where: { electionId: election.id },
          include: [{ model: Candidate, as: "candidates" }],
          transaction: t,
        });

        for (const position of existingPositions) {
          const candidates =
            (position.get("candidates") as { id: string }[] | undefined) ?? [];
          const candidateIds = candidates.map((candidate) => candidate.id);
          if (candidateIds.length > 0) {
            await CandidateQA.destroy({
              where: { candidateId: { [Op.in]: candidateIds } },
              transaction: t,
            });
            await Candidate.destroy({
              where: { id: { [Op.in]: candidateIds } },
              transaction: t,
            });
          }
          await position.destroy({ transaction: t });
        }

        for (let index = 0; index < data.positions.length; index++) {
          const positionInput = data.positions[index];
          const seats =
            positionInput.positionType === "multi_seat"
              ? positionInput.seatsAvailable
              : 1;
          const position = await ElectionPosition.create(
            {
              electionId: election.id,
              title: positionInput.title,
              positionType: positionInput.positionType,
              seatsAvailable: seats,
              position: index,
            },
            { transaction: t }
          );

          for (
            let cIndex = 0;
            cIndex < positionInput.candidates.length;
            cIndex++
          ) {
            const candidateInput = positionInput.candidates[cIndex];
            await Candidate.create(
              {
                electionPositionId: position.id,
                name: candidateInput.name,
                biography: candidateInput.biography ?? null,
                goalsStatement: candidateInput.goalsStatement ?? null,
                status: "approved",
                position: cIndex,
              },
              { transaction: t }
            );
          }
        }
      }

      await election.update(
        {
          ...(data.title !== undefined && { title: data.title }),
          ...(data.description !== undefined && {
            description: data.description,
          }),
          ...(data.ballotSecrecy !== undefined && {
            ballotSecrecy: data.ballotSecrecy,
          }),
          ...(data.votingMethod !== undefined && {
            votingMethod: data.votingMethod,
          }),
          ...(data.eligibilityModel !== undefined && {
            eligibilityModel: data.eligibilityModel,
            ...(data.eligibilityModel !== "member"
              ? { openEnrollment: false }
              : {}),
          }),
          ...(data.requiresAuthorizationRecord !== undefined && {
            requiresAuthorizationRecord: data.requiresAuthorizationRecord,
          }),
          ...(data.recordDate !== undefined && { recordDate }),
          ...(data.quorumRequired !== undefined && {
            quorumRequired: data.quorumRequired,
          }),
          ...(data.quorumCount !== undefined && {
            quorumCount: data.quorumRequired === false ? null : data.quorumCount,
          }),
          ...(data.digitalSignatureRequired !== undefined && {
            digitalSignatureRequired: data.digitalSignatureRequired,
          }),
          ...(data.verificationMethod !== undefined && {
            securityConfig: {
              ...(election.securityConfig ?? {}),
              verificationMethod: data.verificationMethod,
            },
          }),
          ...(data.resultsVisibility !== undefined && {
            resultsVisibility: data.resultsVisibility,
          }),
          ...(data.isTestMode !== undefined && { isTestMode: data.isTestMode }),
          ...(data.openEnrollment !== undefined &&
            (data.eligibilityModel ?? election.eligibilityModel) === "member" && {
              openEnrollment: data.openEnrollment,
            }),
          ...(data.startAt !== undefined && { startAt }),
          ...(data.endAt !== undefined && { endAt }),
        },
        { transaction: t }
      );

      await AuditLog.create(
        {
          actorUserId: userId,
          workspaceId,
          action: "election.updated",
          metadata: {
            electionId: election.id,
            status: election.status,
            positionsUpdated: data.positions !== undefined,
          },
        },
        { transaction: t }
      );
    });
  } catch (err) {
    if (err instanceof Error && err.message === "BALLOTS_CAST") {
      return {
        success: false,
        error:
          "Positions and candidates cannot change after ballots have been cast",
      };
    }
    return { success: false, error: "Failed to update election" };
  }

  revalidateElectionPaths(election.id, election.slug);
  return { success: true };
}

/** Update open/close schedule for draft, open, or closed elections (including live). */
export async function updateElectionSchedule(
  input: unknown
): Promise<ActionResult> {
  const { userId, workspaceId, role } = await requireClientWorkspace();
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const parsed = updateElectionScheduleSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const election = await getScopedElection(parsed.data.electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status === "certified") {
    return {
      success: false,
      error: "Certified elections cannot change open or close dates",
    };
  }

  const data = parsed.data;
  const startAt =
    data.startAt === undefined
      ? election.startAt
      : data.startAt
        ? new Date(data.startAt)
        : null;
  const endAt =
    data.endAt === undefined
      ? election.endAt
      : data.endAt
        ? new Date(data.endAt)
        : null;

  if (startAt && Number.isNaN(startAt.getTime())) {
    return { success: false, error: "Invalid open date" };
  }
  if (endAt && Number.isNaN(endAt.getTime())) {
    return { success: false, error: "Invalid close date" };
  }
  if (startAt && endAt && startAt.getTime() > endAt.getTime()) {
    return {
      success: false,
      error: "Close date must be on or after the open date",
    };
  }
  if (election.recordDate && startAt) {
    const record = new Date(`${election.recordDate}T00:00:00`);
    if (
      !Number.isNaN(record.getTime()) &&
      record.getTime() > startAt.getTime()
    ) {
      return {
        success: false,
        error: "Open date must be on or after the record date",
      };
    }
  }

  const previousStartAt = election.startAt;
  const previousEndAt = election.endAt;

  await election.update({
    ...(data.startAt !== undefined && { startAt }),
    ...(data.endAt !== undefined && { endAt }),
  });

  const { AuditLog } = initDb();
  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "election.schedule_updated",
    metadata: {
      electionId: election.id,
      previousStartAt,
      previousEndAt,
      startAt,
      endAt,
      isTestMode: election.isTestMode,
      status: election.status,
    },
  });

  revalidateElectionPaths(election.id, election.slug);
  return { success: true };
}

export async function publishElection(
  electionId: string
): Promise<ActionResult<{ slug: string }>> {
  const { userId, workspaceId } = await requireClientWorkspace();
  const parsed = electionIdSchema.safeParse({ electionId });
  if (!parsed.success) return { success: false, error: "Invalid election" };

  const election = await getScopedElection(electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status !== "draft") {
    return { success: false, error: "Only draft elections can be published" };
  }

  const { sequelize, ElectionPosition, Candidate, AuditLog } = initDb();

  try {
    await sequelize.transaction(async (t) => {
      const positions = await ElectionPosition.findAll({
        where: { electionId },
        include: [{ model: Candidate, as: "candidates" }],
        transaction: t,
      });

      if (positions.length === 0) {
        throw new Error("NO_POSITIONS");
      }

      const hasApproved = positions.some((position) => {
        const candidates = position.get("candidates") as { status: string }[] | undefined;
        return candidates?.some((candidate) => candidate.status === "approved");
      });
      if (!hasApproved) {
        throw new Error("NO_CANDIDATES");
      }

      await generateVoterRollSnapshot(electionId, t);

      await election.update(
        {
          status: "open",
          publishedAt: election.publishedAt ?? new Date(),
        },
        { transaction: t }
      );

      await AuditLog.create(
        {
          actorUserId: userId,
          workspaceId,
          action: "election.published",
          metadata: { electionId, slug: election.slug },
        },
        { transaction: t }
      );
    });

    revalidateElectionPaths(election.id, election.slug);
    return { success: true, data: { slug: election.slug } };
  } catch (err) {
    if (err instanceof Error && err.message === "NO_POSITIONS") {
      return { success: false, error: "Add at least one position before publishing" };
    }
    if (err instanceof Error && err.message === "NO_CANDIDATES") {
      return {
        success: false,
        error: "Each election needs at least one approved candidate",
      };
    }
    return { success: false, error: "Failed to publish election" };
  }
}

export async function closeElection(
  electionId: string
): Promise<ActionResult> {
  const { userId, workspaceId } = await requireClientWorkspace();
  const parsed = electionIdSchema.safeParse({ electionId });
  if (!parsed.success) return { success: false, error: "Invalid election" };

  const election = await getScopedElection(electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status !== "open") {
    return { success: false, error: "Only open elections can be closed" };
  }

  await election.update({ status: "closed", closedAt: new Date() });

  const { AuditLog } = initDb();
  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "election.closed",
    metadata: { electionId },
  });

  revalidateElectionPaths(election.id, election.slug);
  return { success: true };
}

export async function certifyElection(
  input: unknown
): Promise<ActionResult> {
  const { userId, workspaceId } = await requireClientWorkspace();
  const parsed = certifyElectionSchema.safeParse(
    typeof input === "string" ? { electionId: input } : input
  );
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const election = await getScopedElection(parsed.data.electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status !== "closed") {
    return { success: false, error: "Only closed elections can be certified" };
  }

  const { ElectionVoterRoll, AuditLog } = initDb();
  const blockedCount = await ElectionVoterRoll.count({
    where: {
      electionId: election.id,
      eligibilityStatus: ["pending_authorization", "blocked"],
    },
  });
  if (blockedCount > 0 && !parsed.data.overridePendingAuthorization) {
    return {
      success: false,
      error:
        "Certification is blocked while voter-roll rows are pending authorization or blocked. Provide an override reason to continue.",
    };
  }

  await election.update({ status: "certified", certifiedAt: new Date() });
  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "election.certified",
    metadata: {
      electionId: election.id,
      overridePendingAuthorization: parsed.data.overridePendingAuthorization,
      overrideReason: parsed.data.overrideReason ?? null,
      blockedCount,
    },
  });

  revalidateElectionPaths(election.id, election.slug);
  return { success: true };
}

/** Promote a test election to live (or move it back to test). Allowed until certified. */
export async function setElectionTestMode(
  input: unknown
): Promise<ActionResult> {
  const { userId, workspaceId, role } = await requireClientWorkspace();
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const parsed = setElectionTestModeSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const election = await getScopedElection(parsed.data.electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status === "certified") {
    return {
      success: false,
      error: "Certified elections cannot change test/live mode",
    };
  }

  if (election.isTestMode === parsed.data.isTestMode) {
    return { success: true };
  }

  await election.update({ isTestMode: parsed.data.isTestMode });

  const { AuditLog } = initDb();
  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: parsed.data.isTestMode
      ? "election.moved_to_test"
      : "election.promoted_to_live",
    metadata: { electionId: election.id },
  });

  revalidateElectionPaths(election.id, election.slug);
  return { success: true };
}

/** Switch between open vote and email/voter-roll access. Allowed until certified. */
export async function setElectionOpenEnrollment(
  input: unknown
): Promise<ActionResult> {
  const { userId, workspaceId, role } = await requireClientWorkspace();
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const parsed = setElectionOpenEnrollmentSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const election = await getScopedElection(parsed.data.electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status === "certified") {
    return {
      success: false,
      error: "Certified elections cannot change voting access",
    };
  }
  if (election.eligibilityModel !== "member") {
    return {
      success: false,
      error: "Open vote is only available for one member, one vote elections",
    };
  }

  if (election.openEnrollment === parsed.data.openEnrollment) {
    return { success: true };
  }

  await election.update({ openEnrollment: parsed.data.openEnrollment });

  const { AuditLog } = initDb();
  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: parsed.data.openEnrollment
      ? "election.open_vote_enabled"
      : "election.voter_roll_required",
    metadata: { electionId: election.id },
  });

  revalidateElectionPaths(election.id, election.slug);
  return { success: true };
}

export async function saveAsTemplate(input: {
  electionId: string;
  name: string;
}): Promise<ActionResult<{ templateId: string }>> {
  const { userId, workspaceId, role } = await requireClientWorkspace();
  const { electionMutationBlockedReason } = await import(
    "@/lib/auth/election-access"
  );
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const name = input.name.trim();
  if (!name) return { success: false, error: "Template name is required" };

  const election = await getScopedElection(input.electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };

  const { ElectionPosition, ElectionTemplate, AuditLog } = initDb();
  const positions = await ElectionPosition.findAll({
    where: { electionId: election.id },
    order: [["position", "ASC"]],
  });

  const template = await ElectionTemplate.create({
    workspaceId,
    name,
    createdBy: userId,
    snapshot: {
      titleHint: election.title,
      description: election.description,
      ballotSecrecy: election.ballotSecrecy,
      votingMethod: election.votingMethod,
      eligibilityModel: election.eligibilityModel,
      allowProxyVoting: election.allowProxyVoting,
      requiresAuthorizationRecord: election.requiresAuthorizationRecord,
      quorumRequired: election.quorumRequired,
      quorumCount: election.quorumCount,
      digitalSignatureRequired: election.digitalSignatureRequired,
      resultsVisibility: election.resultsVisibility,
      securityConfig: election.securityConfig,
      positions: positions.map((position) => ({
        title: position.title,
        positionType: position.positionType,
        seatsAvailable: position.seatsAvailable,
      })),
    },
  });

  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "election.template_saved",
    metadata: { electionId: election.id, templateId: template.id },
  });

  revalidateElectionPaths(election.id);
  revalidatePath("/app/elections/new");
  return { success: true, data: { templateId: template.id } };
}

export async function createFromTemplate(input: {
  templateId: string;
  title?: string;
}): Promise<ActionResult<{ electionId: string }>> {
  const { userId, workspaceId, role } = await requireClientWorkspace();
  const { electionMutationBlockedReason } = await import(
    "@/lib/auth/election-access"
  );
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const { ElectionTemplate, Election, ElectionPosition, AuditLog, sequelize } =
    initDb();
  const template = await ElectionTemplate.findOne({
    where: { id: input.templateId, workspaceId },
  });
  if (!template) return { success: false, error: "Template not found" };

  const snapshot = template.snapshot;
  const title =
    input.title?.trim() ||
    `${snapshot.titleHint ?? template.name} (from template)`;

  try {
    const electionId = await sequelize.transaction(async (t) => {
      const election = await Election.create(
        {
          workspaceId,
          title,
          description: snapshot.description ?? null,
          slug: await generateUniqueElectionSlug(title),
          status: "draft",
          ballotSecrecy: snapshot.ballotSecrecy ?? "secret",
          votingMethod: snapshot.votingMethod ?? "plurality",
          eligibilityModel: snapshot.eligibilityModel ?? "member",
          allowProxyVoting: snapshot.allowProxyVoting ?? false,
          requiresAuthorizationRecord:
            snapshot.requiresAuthorizationRecord ?? false,
          quorumRequired: snapshot.quorumRequired ?? false,
          quorumCount: snapshot.quorumCount ?? null,
          digitalSignatureRequired: snapshot.digitalSignatureRequired ?? false,
          resultsVisibility: snapshot.resultsVisibility ?? "after_close",
          securityConfig: snapshot.securityConfig ?? {
            verificationMethod: "email",
          },
          isTestMode: true,
          createdBy: userId,
        },
        { transaction: t }
      );

      for (let index = 0; index < (snapshot.positions ?? []).length; index++) {
        const position = snapshot.positions![index];
        await ElectionPosition.create(
          {
            electionId: election.id,
            title: position.title,
            positionType: position.positionType,
            seatsAvailable: position.seatsAvailable,
            position: index,
          },
          { transaction: t }
        );
      }

      await AuditLog.create(
        {
          actorUserId: userId,
          workspaceId,
          action: "election.created_from_template",
          metadata: { electionId: election.id, templateId: template.id },
        },
        { transaction: t }
      );

      return election.id;
    });

    revalidateElectionPaths(electionId);
    return { success: true, data: { electionId } };
  } catch {
    return { success: false, error: "Failed to create election from template" };
  }
}
