"use server";

import { createHash, randomUUID } from "crypto";
import { headers } from "next/headers";
import { UniqueConstraintError, type Transaction, Op, fn, col, where } from "sequelize";
import { revalidatePath } from "next/cache";
import { initDb } from "@/lib/db";
import { requireClientWorkspace } from "@/lib/auth/require-auth";
import {
  castBallotSchema,
  confirmVoterVerificationSchema,
  generateVoterRollSchema,
  requestVoterVerificationSchema,
  withdrawConsentSchema,
} from "@/lib/validation/ballot";
import { getPublicElectionBySlug } from "@/lib/queries/election-filler-queries";
import { getElectionPositions } from "@/lib/queries/candidate-queries";
import { generateVoterRollSnapshot } from "@/lib/elections/generate-voter-roll";
import { electionMutationBlockedReason } from "@/lib/auth/election-access";
import {
  flagDuplicateAttempt,
  runPostCastAnomalyChecks,
} from "@/lib/elections/post-cast-anomaly";
import { generateReceiptCode } from "@/lib/utils/receipt-code";
import { signVotingToken, verifyVotingToken } from "@/lib/utils/voting-token";
import { hashIp } from "@/lib/utils/ip-hash";
import { hashDigitalSignature } from "@/lib/utils/signature-hash";
import {
  generateOtpCode,
  otpMatches,
  signVerificationChallenge,
  verifyVerificationChallenge,
} from "@/lib/utils/verification-challenge";
import { sendEmail } from "@/lib/email/mailer";
import { sendSms } from "@/lib/sms/send-sms";
import { isRankBasedVotingMethod } from "@/config/elections";
import type { ActionResult } from "@/lib/actions/auth";
import type {
  BallotVerificationMethod,
  DigitalSignatureType,
  VotingMethod,
} from "@/types";

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

function clientIp(): string {
  const headerList = headers();
  const forwarded = headerList.get("x-forwarded-for");
  return forwarded?.split(",")[0]?.trim() ?? "unknown";
}

function verificationMethodFor(
  election: { securityConfig: { verificationMethod?: BallotVerificationMethod } | null }
): BallotVerificationMethod {
  return election.securityConfig?.verificationMethod ?? "email";
}

async function resolveEligibleVoter(input: {
  electionId: string;
  workspaceId: string;
  eligibilityModel: string;
  openEnrollment: boolean;
  email: string;
  phone?: string | null;
}): Promise<
  | { ok: true; voterRollId: string; voterName: string; phone: string | null }
  | { ok: false; error: string }
> {
  const { Contact, ElectionVoterRoll, VotingEntityOwner, AuthorizationRecord } =
    initDb();

  const email = input.email.toLowerCase();
  let contact = await Contact.findOne({
    where: {
      workspaceId: input.workspaceId,
      [Op.and]: where(fn("LOWER", col("email")), email),
    },
  });

  if (
    input.openEnrollment &&
    input.eligibilityModel === "member" &&
    !contact
  ) {
    const localPart = email.split("@")[0] || "Voter";
    try {
      contact = await Contact.create({
        workspaceId: input.workspaceId,
        name: localPart,
        email,
        phone: input.phone?.trim() || null,
        notes: "Auto-enrolled via open election ballot",
      });
    } catch (err) {
      if (!(err instanceof UniqueConstraintError)) throw err;
      contact = await Contact.findOne({
        where: {
          workspaceId: input.workspaceId,
          [Op.and]: where(fn("LOWER", col("email")), email),
        },
      });
    }
  }

  if (!contact) {
    return { ok: false, error: "No matching voter on the roll for this email" };
  }

  if (input.eligibilityModel === "member") {
    let roll = await ElectionVoterRoll.findOne({
      where: {
        electionId: input.electionId,
        voterType: "contact",
        voterRefId: contact.id,
      },
    });

    if (!roll && input.openEnrollment) {
      roll = await ElectionVoterRoll.create({
        electionId: input.electionId,
        voterType: "contact",
        voterRefId: contact.id,
        weight: 1,
        eligibilityStatus: "eligible",
        hasVoted: false,
      });
    }

    if (!roll) {
      return { ok: false, error: "No matching voter on the roll for this email" };
    }
    if (roll.eligibilityStatus !== "eligible") {
      return { ok: false, error: "You are not eligible to vote in this election" };
    }
    if (roll.hasVoted) {
      return {
        ok: false,
        error: "A ballot has already been recorded for this voter",
      };
    }
    if (input.phone?.trim() && !contact.phone) {
      await contact.update({ phone: input.phone.trim() });
    }
    return {
      ok: true,
      voterRollId: roll.id,
      voterName: contact.name,
      phone: contact.phone,
    };
  }

  const ownerships = await VotingEntityOwner.findAll({
    where: { contactId: contact.id },
  });
  if (ownerships.length === 0) {
    return { ok: false, error: "No matching voter on the roll for this email" };
  }

  const entityIds = ownerships.map((owner) => owner.votingEntityId);
  const rolls = await ElectionVoterRoll.findAll({
    where: {
      electionId: input.electionId,
      voterType: "voting_entity",
      voterRefId: entityIds,
    },
  });
  if (rolls.length === 0) {
    return { ok: false, error: "No matching voter on the roll for this email" };
  }

  const authRecords = await AuthorizationRecord.findAll({
    where: {
      votingEntityId: entityIds,
      status: { [Op.ne]: "superseded" },
    },
    order: [["createdAt", "DESC"]],
  });
  const latestAuth = new Map<string, (typeof authRecords)[number]>();
  for (const record of authRecords) {
    if (!latestAuth.has(record.votingEntityId)) {
      latestAuth.set(record.votingEntityId, record);
    }
  }

  const eligibleRolls = rolls.filter((roll) => {
    if (roll.eligibilityStatus !== "eligible" || roll.hasVoted) return false;
    const auth = latestAuth.get(roll.voterRefId);
    if (auth?.designatedVoterContactId) {
      return auth.designatedVoterContactId === contact.id;
    }
    return true;
  });

  if (eligibleRolls.length === 0) {
    const anyPending = rolls.some(
      (roll) => roll.eligibilityStatus === "pending_authorization"
    );
    if (anyPending) {
      return {
        ok: false,
        error:
          "This voting entity is pending authorization and cannot vote yet",
      };
    }
    if (rolls.every((roll) => roll.hasVoted)) {
      return {
        ok: false,
        error: "A ballot has already been recorded for this voter",
      };
    }
    return { ok: false, error: "You are not eligible to vote in this election" };
  }

  const ownershipByEntity = new Map(
    ownerships.map((owner) => [owner.votingEntityId, owner])
  );
  const chosen =
    eligibleRolls.find((roll) => ownershipByEntity.get(roll.voterRefId)?.isPrimary) ??
    eligibleRolls[0];

  return {
    ok: true,
    voterRollId: chosen.id,
    voterName: contact.name,
    phone: contact.phone,
  };
}

export async function generateVoterRoll(
  electionId: string
): Promise<ActionResult<{ added: number }>> {
  const { workspaceId, role } = await requireClientWorkspace();
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const parsed = generateVoterRollSchema.safeParse({ electionId });
  if (!parsed.success) return { success: false, error: "Invalid election" };

  const { sequelize, Election } = initDb();
  const election = await Election.findOne({
    where: { id: electionId, workspaceId },
  });
  if (!election) return { success: false, error: "Election not found" };
  if (election.status === "certified") {
    return {
      success: false,
      error: "Certified elections cannot regenerate the voter roll",
    };
  }

  try {
    const added = await sequelize.transaction(async (t: Transaction) =>
      generateVoterRollSnapshot(electionId, t, {
        // Manual regenerate should pick up contacts added after the record date.
        includeContactsAfterRecordDate: true,
      })
    );
    revalidatePath(`/app/elections/${electionId}`);
    return { success: true, data: { added } };
  } catch {
    return { success: false, error: "Failed to generate voter roll" };
  }
}

export async function requestVoterVerification(input: unknown): Promise<
  ActionResult<{
    challengeToken: string;
    delivery: BallotVerificationMethod;
    maskedDestination: string;
    debugCode?: string;
  }>
> {
  const parsed = requestVoterVerificationSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const publicElection = await getPublicElectionBySlug(parsed.data.slug);
  if (!publicElection) {
    return { success: false, error: "Election not found" };
  }
  if (!publicElection.votingOpen) {
    return {
      success: false,
      error: publicElection.closedReason ?? "Voting is not open",
    };
  }

  const { Election } = initDb();
  const election = await Election.findByPk(publicElection.id);
  if (!election) return { success: false, error: "Election not found" };

  const resolved = await resolveEligibleVoter({
    electionId: election.id,
    workspaceId: election.workspaceId,
    eligibilityModel: election.eligibilityModel,
    openEnrollment: election.openEnrollment,
    email: parsed.data.email,
    phone: parsed.data.phone,
  });
  if (!resolved.ok) return { success: false, error: resolved.error };

  const method = verificationMethodFor(election);
  const phone =
    parsed.data.phone?.trim() || resolved.phone || null;

  if ((method === "sms_2fa" || method === "email_sms") && !phone) {
    return {
      success: false,
      error: "A phone number is required for SMS verification",
    };
  }

  const code = generateOtpCode();
  const challengeToken = signVerificationChallenge({
    electionId: election.id,
    voterRollId: resolved.voterRollId,
    email: parsed.data.email.toLowerCase(),
    phone,
    verificationMethod: method,
    code,
  });

  const subject = `${election.title} — your voting code`;
  const html = `<p>Your SurveyStronghold voting verification code is:</p>
<p style="font-size:24px;font-weight:700;letter-spacing:4px">${code}</p>
<p>This code expires in 10 minutes.</p>`;

  let delivered = false;
  let logged = false;

  if (method === "email" || method === "email_sms") {
    const emailResult = await sendEmail({
      to: parsed.data.email,
      subject,
      html,
    });
    if (emailResult.ok) delivered = true;
    else {
      console.info(
        `[voter-otp:email-fallback] to=${parsed.data.email} code=${code} error=${emailResult.error}`
      );
      logged = true;
      delivered = true;
    }
  }

  if (method === "sms_2fa" || method === "email_sms") {
    const smsResult = await sendSms({
      to: phone!,
      body: `${election.title}: your voting code is ${code}. Expires in 10 minutes.`,
    });
    if (smsResult.ok) {
      delivered = true;
      if (smsResult.logged) logged = true;
    } else if (method === "sms_2fa") {
      return {
        success: false,
        error: smsResult.error ?? "Could not send SMS verification code",
      };
    }
  }

  if (!delivered) {
    return { success: false, error: "Could not deliver a verification code" };
  }

  const maskedDestination =
    method === "sms_2fa"
      ? maskPhone(phone!)
      : method === "email_sms"
        ? `${maskEmail(parsed.data.email)} / ${maskPhone(phone!)}`
        : maskEmail(parsed.data.email);

  return {
    success: true,
    data: {
      challengeToken,
      delivery: method,
      maskedDestination,
      ...(process.env.NODE_ENV !== "production" || logged
        ? { debugCode: code }
        : {}),
    },
  };
}

export async function confirmVoterVerification(input: unknown): Promise<
  ActionResult<{ votingToken: string; voterName: string }>
> {
  const parsed = confirmVoterVerificationSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const challenge = verifyVerificationChallenge(parsed.data.challengeToken);
  if (!challenge) {
    return {
      success: false,
      error: "Verification expired. Request a new code.",
    };
  }
  if (!otpMatches(parsed.data.code, challenge.codeHash)) {
    return { success: false, error: "Invalid verification code" };
  }

  const { Election, ElectionVoterRoll, Contact } = initDb();
  const election = await Election.findByPk(challenge.electionId);
  if (!election || election.status !== "open") {
    return { success: false, error: "This election is not accepting ballots" };
  }

  const roll = await ElectionVoterRoll.findByPk(challenge.voterRollId);
  if (
    !roll ||
    roll.electionId !== election.id ||
    roll.eligibilityStatus !== "eligible" ||
    roll.hasVoted
  ) {
    return { success: false, error: "You are not eligible to vote" };
  }

  let voterName = "Voter";
  if (roll.voterType === "contact") {
    const contact = await Contact.findByPk(roll.voterRefId);
    voterName = contact?.name ?? voterName;
  } else {
    const contact = await Contact.findOne({
      where: {
        workspaceId: election.workspaceId,
        [Op.and]: where(
          fn("LOWER", col("email")),
          challenge.email.toLowerCase()
        ),
      },
    });
    voterName = contact?.name ?? voterName;
  }

  return {
    success: true,
    data: {
      votingToken: signVotingToken(
        election.id,
        roll.id,
        challenge.verificationMethod
      ),
      voterName,
    },
  };
}

/** Legacy single-step verify — still used nowhere preferred; kept for compatibility. */
export async function verifyVoter(input: {
  slug: string;
  email: string;
  phone?: string | null;
}): Promise<ActionResult<{ votingToken: string; voterName: string }>> {
  const requested = await requestVoterVerification(input);
  if (!requested.success || !requested.data) {
    return {
      success: false,
      error: requested.success ? "Verification failed" : requested.error,
    };
  }
  if (!requested.data.debugCode) {
    return {
      success: false,
      error: "Enter the verification code sent to you to continue",
    };
  }
  return confirmVoterVerification({
    challengeToken: requested.data.challengeToken,
    code: requested.data.debugCode,
  });
}

function validateSelections(input: {
  votingMethod: VotingMethod;
  positions: Awaited<ReturnType<typeof getElectionPositions>>;
  selections: {
    electionPositionId: string;
    candidateId?: string | null;
    rank?: number | null;
    writeInText?: string | null;
  }[];
}): string | null {
  const rankBased = isRankBasedVotingMethod(input.votingMethod);
  const byPosition = new Map<string, typeof input.selections>();

  for (const selection of input.selections) {
    const list = byPosition.get(selection.electionPositionId) ?? [];
    list.push(selection);
    byPosition.set(selection.electionPositionId, list);
  }

  for (const position of input.positions) {
    const approvedIds = new Set(
      position.candidates
        .filter((candidate) => candidate.status === "approved")
        .map((candidate) => candidate.id)
    );
    const selected = byPosition.get(position.id) ?? [];
    if (selected.length === 0) {
      return `Select a candidate for ${position.title}`;
    }

    const candidateIds = selected
      .map((item) => item.candidateId)
      .filter((id): id is string => Boolean(id));
    if (new Set(candidateIds).size !== candidateIds.length) {
      return `Duplicate candidate selection on ${position.title}`;
    }

    for (const item of selected) {
      if (item.candidateId && !approvedIds.has(item.candidateId)) {
        return "One or more selections are not approved candidates";
      }
      if (rankBased) {
        if (item.rank == null) {
          return `Rank is required for ${position.title}`;
        }
      } else if (item.rank != null) {
        return "Rank is only valid for ranked voting methods";
      }
    }

    if (rankBased) {
      const ranks = selected
        .map((item) => item.rank)
        .filter((rank): rank is number => rank != null);
      if (new Set(ranks).size !== ranks.length) {
        return `Ranks must be unique for ${position.title}`;
      }
    } else if (input.votingMethod === "approval") {
      if (selected.length > approvedIds.size) {
        return `Too many selections for ${position.title}`;
      }
    } else if (position.positionType === "multi_seat") {
      if (selected.length > position.seatsAvailable) {
        return `Select up to ${position.seatsAvailable} candidates for ${position.title}`;
      }
    } else if (selected.length !== 1) {
      return `Select exactly one option for ${position.title}`;
    }
  }

  for (const positionId of Array.from(byPosition.keys())) {
    if (!input.positions.some((position) => position.id === positionId)) {
      return "Invalid position on ballot";
    }
  }

  return null;
}

export async function castBallot(input: unknown): Promise<
  ActionResult<{ receiptCode: string }>
> {
  const parsed = castBallotSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: firstZodError(parsed.error) };
  }

  const token = verifyVotingToken(parsed.data.votingToken);
  if (!token) {
    return { success: false, error: "Your voting session expired. Verify again." };
  }

  const {
    sequelize,
    Election,
    ElectionVoterRoll,
    BallotCastEvent,
    Ballot,
    BallotSelection,
    DigitalSignature,
  } = initDb();

  const election = await Election.findByPk(token.electionId);
  if (!election || election.status !== "open") {
    return { success: false, error: "This election is not accepting ballots" };
  }

  const now = Date.now();
  if (election.startAt && election.startAt.getTime() > now) {
    return { success: false, error: "Voting has not opened yet" };
  }
  if (election.endAt && election.endAt.getTime() < now) {
    return { success: false, error: "The voting window has ended" };
  }

  if (election.digitalSignatureRequired && parsed.data.consentAccepted !== true) {
    return { success: false, error: "Consent is required to cast this ballot" };
  }

  const positions = await getElectionPositions(election.id);
  const selectionError = validateSelections({
    votingMethod: election.votingMethod,
    positions,
    selections: parsed.data.selections,
  });
  if (selectionError) {
    return { success: false, error: selectionError };
  }

  const headerList = headers();
  const rawIp = clientIp();
  const ipHash = hashIp(rawIp);
  const userAgent = headerList.get("user-agent") ?? null;
  const deviceFingerprintHash = userAgent
    ? hashIp(`ua:${userAgent}`)
    : null;

  const signatureType: DigitalSignatureType =
    parsed.data.signatureType ?? "checkbox_consent";
  const signerName =
    parsed.data.signerName?.trim() ||
    (signatureType === "checkbox_consent" ? "Consenting voter" : "");

  try {
    const receiptCode = await sequelize.transaction(async (t) => {
      const roll = await ElectionVoterRoll.findByPk(token.voterRollId, {
        transaction: t,
        lock: t.LOCK.UPDATE,
      });
      if (!roll || roll.electionId !== election.id) {
        throw new Error("ROLL_NOT_FOUND");
      }
      if (roll.eligibilityStatus !== "eligible") {
        throw new Error("NOT_ELIGIBLE");
      }
      if (roll.hasVoted) {
        throw new Error("ALREADY_VOTED");
      }

      const castEvent = await BallotCastEvent.create(
        {
          electionId: election.id,
          voterRollId: roll.id,
          castAt: new Date(),
          ipHash,
          deviceFingerprintHash,
          userAgent,
          verificationMethod: token.verificationMethod,
          digitalSignatureId: null,
        },
        { transaction: t }
      );

      if (election.digitalSignatureRequired) {
        const stamped = new Date();
        const signature = await DigitalSignature.create(
          {
            ballotCastEventId: castEvent.id,
            signerName,
            signatureType,
            signatureHash: hashDigitalSignature({
              signerName,
              signatureType,
              electionId: election.id,
              voterRollId: roll.id,
              drawnPath: parsed.data.drawnSignaturePath,
              consentAccepted: parsed.data.consentAccepted === true,
            }),
            ipAddress: createHash("sha256").update(rawIp).digest("hex").slice(0, 64),
            dateStamp: stamped.toISOString().slice(0, 10),
            timeStamp: stamped,
            consentWithdrawnAt: null,
          },
          { transaction: t }
        );
        await castEvent.update(
          { digitalSignatureId: signature.id },
          { transaction: t }
        );
      }

      let code = generateReceiptCode();
      for (let attempt = 0; attempt < 5; attempt++) {
        const existing = await Ballot.findOne({
          where: { receiptCode: code },
          transaction: t,
        });
        if (!existing) break;
        code = generateReceiptCode();
      }

      const ballot = await Ballot.create(
        {
          electionId: election.id,
          castToken: randomUUID(),
          receiptCode: code,
          submittedAt: new Date(),
          voterRollId: election.ballotSecrecy === "open" ? roll.id : null,
        },
        { transaction: t }
      );

      for (const selection of parsed.data.selections) {
        await BallotSelection.create(
          {
            ballotId: ballot.id,
            electionPositionId: selection.electionPositionId,
            candidateId: selection.candidateId ?? null,
            rank: selection.rank ?? null,
            writeInText: selection.writeInText ?? null,
          },
          { transaction: t }
        );
      }

      await roll.update(
        { hasVoted: true, votedAt: new Date() },
        { transaction: t }
      );

      return code;
    });

    void runPostCastAnomalyChecks({
      electionId: election.id,
      voterRollId: token.voterRollId,
      ipHash,
      deviceFingerprintHash,
    }).catch((err) => {
      console.error("post-cast anomaly check failed", err);
    });

    void import("@/lib/actions/election-distribute")
      .then(({ markDeliveriesVoted }) => markDeliveriesVoted(token.voterRollId))
      .catch(() => undefined);

    revalidatePath(`/app/elections/${election.id}`);
    revalidatePath(`/e/${election.slug}`);
    return { success: true, data: { receiptCode } };
  } catch (err) {
    if (err instanceof UniqueConstraintError || (err instanceof Error && err.message === "ALREADY_VOTED")) {
      void flagDuplicateAttempt({
        electionId: election.id,
        voterRollId: token.voterRollId,
        ipHash,
        deviceFingerprintHash,
      }).catch(() => undefined);
      return { success: false, error: "A ballot has already been recorded for this voter" };
    }
    if (err instanceof Error) {
      if (err.message === "NOT_ELIGIBLE") {
        return { success: false, error: "You are not eligible to vote in this election" };
      }
      if (err.message === "ROLL_NOT_FOUND") {
        return { success: false, error: "Voter record not found" };
      }
    }
    return { success: false, error: "Failed to cast ballot" };
  }
}

export async function withdrawConsent(
  ballotCastEventId: string
): Promise<ActionResult> {
  const { userId, workspaceId } = await requireClientWorkspace();
  const parsed = withdrawConsentSchema.safeParse({ ballotCastEventId });
  if (!parsed.success) return { success: false, error: "Invalid cast event" };

  const { BallotCastEvent, DigitalSignature, Election, AuditLog } = initDb();
  const castEvent = await BallotCastEvent.findByPk(parsed.data.ballotCastEventId);
  if (!castEvent) return { success: false, error: "Cast event not found" };

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

  if (!castEvent.digitalSignatureId) {
    return { success: false, error: "No digital signature is attached to this cast" };
  }

  const signature = await DigitalSignature.findByPk(castEvent.digitalSignatureId);
  if (!signature) return { success: false, error: "Signature not found" };
  if (signature.consentWithdrawnAt) {
    return { success: false, error: "Consent was already withdrawn" };
  }

  await signature.update({ consentWithdrawnAt: new Date() });
  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "digital_signature.consent_withdrawn",
    metadata: {
      ballotCastEventId: castEvent.id,
      digitalSignatureId: signature.id,
      electionId: election.id,
    },
  });

  revalidatePath(`/app/elections/${election.id}`);
  return { success: true };
}

function maskEmail(email: string): string {
  const [local, domain] = email.split("@");
  if (!local || !domain) return "***";
  const visible = local.slice(0, 1);
  return `${visible}***@${domain}`;
}

function maskPhone(phone: string): string {
  const digits = phone.replace(/\D/g, "");
  if (digits.length < 4) return "***";
  return `***${digits.slice(-4)}`;
}
