"use server";

import { revalidatePath } from "next/cache";
import { Op } from "sequelize";
import { initDb } from "@/lib/db";
import { requireClientWorkspace } from "@/lib/auth/require-auth";
import { electionMutationBlockedReason } from "@/lib/auth/election-access";
import { getScopedElection } from "@/lib/queries/election-queries";
import { sendEmail } from "@/lib/email/mailer";
import { slugify, randomSlugSuffix } from "@/lib/utils/slugify";
import { z } from "zod";
import type { ActionResult } from "@/lib/actions/auth";

const inviteSchema = z.object({
  electionId: z.string().uuid(),
  subject: z.string().trim().min(1).max(255),
  body: z.string().trim().min(1).max(10000),
  eligibilityStatus: z
    .enum(["eligible", "pending_authorization", "blocked", "all"])
    .default("eligible"),
});

const MAX_SHARE_EMAIL_RECIPIENTS = 50;

const shareEmailSchema = z.object({
  electionId: z.string().uuid(),
  subject: z.string().trim().min(1).max(255),
  body: z.string().trim().min(1).max(10000),
  recipients: z.string().trim().min(1).max(20000),
});

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

function parseEmails(raw: string): string[] {
  return Array.from(
    new Set(
      raw
        .split(/[\n,;]+/)
        .map((e) => e.trim().toLowerCase())
        .filter((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))
    )
  );
}

/** Send ballot link to pasted email addresses (survey-style share). */
export async function sendElectionShareEmails(input: unknown): Promise<
  ActionResult<{ sent: number; failed: number }>
> {
  const { userId, workspaceId, role } = await requireClientWorkspace();
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const parsed = shareEmailSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: parsed.error.issues[0]?.message ?? "Invalid input" };
  }

  const election = await getScopedElection(parsed.data.electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status !== "open") {
    return { success: false, error: "Publish the election before sending invites" };
  }

  const emails = parseEmails(parsed.data.recipients);
  if (emails.length === 0) {
    return { success: false, error: "No valid email addresses found" };
  }
  if (emails.length > MAX_SHARE_EMAIL_RECIPIENTS) {
    return {
      success: false,
      error: `Maximum ${MAX_SHARE_EMAIL_RECIPIENTS} recipients per send`,
    };
  }

  const baseUrl =
    process.env.NEXT_PUBLIC_APP_URL ??
    process.env.NEXTAUTH_URL ??
    "http://localhost:3000";
  const ballotUrl = `${baseUrl}/e/${election.slug}`;
  const htmlBody = `
    <p>${escapeHtml(parsed.data.body).replace(/\n/g, "<br>")}</p>
    <p><a href="${ballotUrl}">${ballotUrl}</a></p>
  `;

  let sent = 0;
  let failed = 0;
  for (const to of emails) {
    const result = await sendEmail({
      to,
      subject: parsed.data.subject,
      html: htmlBody,
    });
    if (result.ok) sent += 1;
    else failed += 1;
  }

  const { AuditLog } = initDb();
  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "election.share_emails_sent",
    metadata: {
      electionId: election.id,
      sent,
      failed,
      recipientCount: emails.length,
    },
  });

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

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

  const parsed = inviteSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: parsed.error.issues[0]?.message ?? "Invalid input" };
  }

  const election = await getScopedElection(parsed.data.electionId, workspaceId);
  if (!election) return { success: false, error: "Election not found" };
  if (election.status !== "open") {
    return { success: false, error: "Publish the election before sending invites" };
  }

  const {
    Collector,
    ElectionVoterRoll,
    Contact,
    VotingEntityOwner,
    EmailCampaignDelivery,
    AuditLog,
  } = initDb();

  let collector = await Collector.findOne({
    where: { electionId: election.id, type: "email_campaign" },
  });
  if (!collector) {
    collector = await Collector.create({
      electionId: election.id,
      surveyId: null,
      type: "email_campaign",
      slug: `${slugify(election.title)}-email-${randomSlugSuffix(6)}`,
      isActive: true,
      config: {
        emailSubject: parsed.data.subject,
        emailBody: parsed.data.body,
      },
    });
  }

  const rollWhere: Record<string, unknown> = { electionId: election.id };
  if (parsed.data.eligibilityStatus !== "all") {
    rollWhere.eligibilityStatus = parsed.data.eligibilityStatus;
  }

  const rolls = await ElectionVoterRoll.findAll({ where: rollWhere });
  const baseUrl =
    process.env.NEXT_PUBLIC_APP_URL ??
    process.env.NEXTAUTH_URL ??
    "http://localhost:3000";
  const ballotUrl = `${baseUrl}/e/${election.slug}`;

  let sent = 0;
  let failed = 0;
  let skipped = 0;

  for (const roll of rolls) {
    let email: string | null = null;

    if (roll.voterType === "contact") {
      const contact = await Contact.findByPk(roll.voterRefId);
      email = contact?.email ?? null;
    } else if (roll.voterType === "voting_entity") {
      const owner = await VotingEntityOwner.findOne({
        where: { votingEntityId: roll.voterRefId, isPrimary: true },
      });
      if (owner) {
        const contact = await Contact.findByPk(owner.contactId);
        email = contact?.email ?? null;
      }
      if (!email) {
        const anyOwner = await VotingEntityOwner.findOne({
          where: { votingEntityId: roll.voterRefId },
        });
        if (anyOwner) {
          const contact = await Contact.findByPk(anyOwner.contactId);
          email = contact?.email ?? null;
        }
      }
    }

    if (!email) {
      skipped += 1;
      continue;
    }

    const html = `
      <p>${escapeHtml(parsed.data.body).replace(/\n/g, "<br>")}</p>
      <p><a href="${ballotUrl}">${ballotUrl}</a></p>
    `;
    const result = await sendEmail({
      to: email,
      subject: parsed.data.subject,
      html,
    });

    if (!result.ok) {
      failed += 1;
      continue;
    }

    sent += 1;
    await EmailCampaignDelivery.findOrCreate({
      where: { collectorId: collector.id, voterRollId: roll.id },
      defaults: {
        collectorId: collector.id,
        voterRollId: roll.id,
        status: "sent",
        sentAt: new Date(),
      },
    });
    await roll.update({ invitationSentAt: new Date() });
  }

  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "election.email_campaign_sent",
    metadata: {
      electionId: election.id,
      sent,
      failed,
      skipped,
    },
  });

  revalidatePath(`/app/elections/${election.id}`);
  return { success: true, data: { sent, failed, skipped } };
}

export async function inviteElectionObserver(input: {
  electionId: string;
  email: string;
  name?: string;
}): Promise<ActionResult<{ observerId: string }>> {
  const { userId, workspaceId, role } = await requireClientWorkspace();
  const blocked = electionMutationBlockedReason(role);
  if (blocked) return { success: false, error: blocked };

  const email = input.email.trim().toLowerCase();
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    return { success: false, error: "Enter a valid email address" };
  }

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

  const { User, ElectionObserver, AuditLog } = initDb();
  let user = await User.findOne({ where: { email } });
  if (!user) {
    user = await User.create({
      name: input.name?.trim() || email.split("@")[0] || "Observer",
      email,
      passwordHash: null,
      role: "election_observer",
      workspaceId,
      isActive: true,
    });
  } else if (user.role === "respondent") {
    await user.update({ role: "election_observer", workspaceId });
  } else if (
    user.role !== "election_observer" &&
    user.role !== "client" &&
    user.role !== "super_admin"
  ) {
    return { success: false, error: "That account cannot be invited as an observer" };
  }

  const [observer, created] = await ElectionObserver.findOrCreate({
    where: { electionId: election.id, userId: user.id },
    defaults: {
      electionId: election.id,
      userId: user.id,
      invitedAt: new Date(),
      revokedAt: null,
    },
  });

  if (!created) {
    if (observer.revokedAt) {
      await observer.update({ revokedAt: null, invitedAt: new Date() });
    } else {
      return { success: false, error: "This user is already an observer" };
    }
  }

  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "election.observer_invited",
    metadata: { electionId: election.id, observerUserId: user.id },
  });

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

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

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

  const { ElectionObserver, AuditLog } = initDb();
  const observer = await ElectionObserver.findOne({
    where: { id: input.observerId, electionId: election.id },
  });
  if (!observer) return { success: false, error: "Observer not found" };

  await observer.update({ revokedAt: new Date() });
  await AuditLog.create({
    actorUserId: userId,
    workspaceId,
    action: "election.observer_revoked",
    metadata: { electionId: election.id, observerId: observer.id },
  });

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

/** Mark invitation deliveries as voted after a successful cast. */
export async function markDeliveriesVoted(voterRollId: string): Promise<void> {
  const { EmailCampaignDelivery } = initDb();
  await EmailCampaignDelivery.update(
    { status: "voted" },
    {
      where: {
        voterRollId,
        status: { [Op.ne]: "voted" },
      },
    }
  );
}
