import { Hono } from "hono";
import { createFactory } from "hono/factory";
import {
  ensureActiveClientAccess,
  normalizeClientEmail,
  revokeClientAccess,
  upsertClientByEmail,
} from "../helpers/clientAccounts.js";
import { createClientMagicLink } from "../helpers/clientMagicLinks.js";
import { checkTabloAdmin } from "../helpers/helpers.js";
import { MiddlewareManager } from "../middlewares/middleware.js";
import type { AuthEnv, BaseEnv } from "../types/app.types.js";

const authFactory = createFactory<AuthEnv>();
const publicFactory = createFactory<BaseEnv>();

const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "");

const findInviteByToken = async (token: string, supabase: BaseEnv["Variables"]["supabase"]) =>
  supabase
    .from("client_invites")
    .select(
      "id, tablo_id, invited_email, invited_by, invite_type, is_pending, expires_at, used_at, cancelled_at, setup_completed_at"
    )
    .eq("invite_token", token)
    .maybeSingle();

const validateSetupInvite = async (token: string, supabase: BaseEnv["Variables"]["supabase"]) => {
  const { data: invite, error } = await findInviteByToken(token, supabase);

  if (error) {
    return { status: 500 as const, body: { error: error.message } };
  }

  if (!invite || invite.invite_type !== "setup" || !invite.is_pending) {
    return { status: 404 as const, body: { error: "Invite not found or already used" } };
  }

  if (invite.cancelled_at || invite.used_at || invite.setup_completed_at) {
    return { status: 404 as const, body: { error: "Invite not found or already used" } };
  }

  if (invite.expires_at && new Date(invite.expires_at) < new Date()) {
    return { status: 410 as const, body: { error: "This invite has expired" } };
  }

  return { status: 200 as const, invite };
};

const sendSetupEmail = async (
  transporter: BaseEnv["Variables"]["transporter"],
  input: { email: string; setupUrl: string }
) => {
  await transporter.sendMail({
    from: "Xtablo <noreply@xtablo.com>",
    to: input.email,
    subject: "Configurez votre accès client Xtablo",
    html: `
      <h2>Vous avez été invité sur Xtablo</h2>
      <p>Bonjour,</p>
      <p>Utilisez le lien ci-dessous pour accéder à votre espace client :</p>
      <p><a href="${input.setupUrl}">Ouvrir mon espace client</a></p>
      <p>Ce lien est à usage unique.</p>
    `,
  });
};

/** POST /:tabloId — Create a client invite (admin only) */
const createClientInvite = (
  middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>,
  config: {
    clientsUrl: string;
    jwtSecret: string;
    ttlMinutes: number;
  }
) =>
  authFactory.createHandlers(middlewareManager.regularUserCheck, checkTabloAdmin, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const transporter = c.get("transporter");
    const tabloId = c.req.param("tabloId");

    const body = await c.req.json();
    const rawEmail = normalizeClientEmail(String(body.email || ""));

    if (!rawEmail || !isValidEmail(rawEmail)) {
      return c.json({ error: "A valid email is required" }, 400);
    }

    const { data: existingProfile, error: existingProfileError } = await supabase
      .from("profiles")
      .select("id")
      .eq("email", rawEmail)
      .maybeSingle();

    if (existingProfileError) {
      return c.json({ error: existingProfileError.message }, 500);
    }

    if (existingProfile) {
      return c.json({ error: "This email already belongs to a main app account" }, 409);
    }

    const clientResult = await upsertClientByEmail(supabase, rawEmail);
    if (clientResult.error || !clientResult.client) {
      return c.json({ error: clientResult.error ?? "Failed to create client account" }, 500);
    }

    const accessResult = await ensureActiveClientAccess(supabase, {
      clientId: clientResult.client.id,
      grantedBy: user.id,
      tabloId,
    });

    if (!accessResult.success) {
      return c.json({ error: accessResult.error ?? "Failed to grant client access" }, 500);
    }

    const magicLinkResult = await createClientMagicLink(supabase, {
      clientId: clientResult.client.id,
      createdBy: user.id,
      email: clientResult.client.email,
      expiresInMinutes: config.ttlMinutes,
      jwtSecret: config.jwtSecret,
      purpose: "invite",
      redirectTo: `/tablo/${tabloId}`,
      tabloId,
    });

    if (magicLinkResult.error || !magicLinkResult.token) {
      return c.json({ error: magicLinkResult.error ?? "Failed to create invite magic link" }, 500);
    }

    try {
      await sendSetupEmail(transporter, {
        email: rawEmail,
        setupUrl: `${trimTrailingSlash(config.clientsUrl)}/auth/exchange?token=${encodeURIComponent(
          magicLinkResult.token
        )}`,
      });
    } catch (emailError) {
      console.error("Failed to send client invite email:", emailError);
    }

    return c.json({ success: true, inviteMode: "setup" as const });
  });

/** GET /setup/:token — Validate a setup invite token */
const getSetupInvite = () =>
  publicFactory.createHandlers(async (c) => {
    const supabase = c.get("supabase");
    const token = c.req.param("token");

    const validation = await validateSetupInvite(token, supabase);
    if (validation.status !== 200) {
      return c.json(validation.body, validation.status);
    }

    return c.json({
      email: validation.invite.invited_email,
      tabloId: validation.invite.tablo_id,
    });
  });

/** POST /setup/:token — Complete one-time password setup */
const completeSetupInvite = () =>
  publicFactory.createHandlers(async (c) => {
    const supabase = c.get("supabase");
    const token = c.req.param("token");
    const body = await c.req.json();
    const password = String(body.password || "");

    if (password.length < 8) {
      return c.json({ error: "Password must be at least 8 characters long" }, 400);
    }

    const validation = await validateSetupInvite(token, supabase);
    if (validation.status !== 200) {
      return c.json(validation.body, validation.status);
    }

    const invite = validation.invite;
    const { data: usersData, error: listUsersError } = await supabase.auth.admin.listUsers();
    if (listUsersError) {
      return c.json({ error: listUsersError.message }, 500);
    }

    // biome-ignore lint/suspicious/noExplicitAny: admin.listUsers returns typed data at runtime
    const users = usersData as any;
    // biome-ignore lint/suspicious/noExplicitAny: admin user type
    const clientUser = users?.users?.find((candidate: any) => {
      return candidate.email?.toLowerCase() === invite.invited_email?.toLowerCase();
    });

    if (!clientUser?.id) {
      return c.json({ error: "Client account not found" }, 404);
    }

    const { error: updateUserError } = await supabase.auth.admin.updateUserById(clientUser.id, {
      password,
    });
    if (updateUserError) {
      return c.json({ error: updateUserError.message }, 500);
    }

    const completedAt = new Date().toISOString();

    const { error: updateProfileError } = await supabase
      .from("profiles")
      .update({ client_onboarded_at: completedAt, is_client: true })
      .eq("id", clientUser.id);
    if (updateProfileError) {
      return c.json({ error: updateProfileError.message }, 500);
    }

    const { error: consumeInviteError } = await supabase
      .from("client_invites")
      .update({
        is_pending: false,
        used_at: completedAt,
        setup_completed_at: completedAt,
      })
      .eq("id", invite.id)
      .eq("is_pending", true);
    if (consumeInviteError) {
      return c.json({ error: consumeInviteError.message }, 500);
    }

    return c.json({
      success: true,
      email: invite.invited_email,
      tabloId: invite.tablo_id,
    });
  });

/** GET /:tabloId/pending — List pending client invites (admin only) */
const getPendingClientInvites = (
  middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>
) =>
  authFactory.createHandlers(middlewareManager.regularUserCheck, checkTabloAdmin, async (c) => {
    const supabase = c.get("supabase");
    const tabloId = c.req.param("tabloId");

    const { data: invites, error } = await supabase
      .from("client_magic_links")
      .select("id, email, expires_at, created_at")
      .eq("tablo_id", tabloId)
      .eq("purpose", "invite")
      .is("consumed_at", null)
      .order("created_at", { ascending: false });

    if (error) {
      return c.json({ error: error.message }, 500);
    }

    return c.json({
      invites:
        invites?.map((invite) => ({
          created_at: invite.created_at,
          expires_at: invite.expires_at,
          id: invite.id,
          invited_email: invite.email,
          is_pending: true,
        })) ?? [],
    });
  });

/** DELETE /:tabloId/:inviteId — Cancel a client invite (admin only) */
const cancelClientInvite = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  authFactory.createHandlers(middlewareManager.regularUserCheck, checkTabloAdmin, async (c) => {
    const supabase = c.get("supabase");
    const tabloId = c.req.param("tabloId");
    const inviteId = Number(c.req.param("inviteId"));

    if (!Number.isInteger(inviteId) || inviteId <= 0) {
      return c.json({ error: "Invalid invite id" }, 400);
    }

    const { data: invite, error: inviteError } = await supabase
      .from("client_magic_links")
      .select("id, client_id, consumed_at, purpose")
      .eq("id", inviteId)
      .eq("tablo_id", tabloId)
      .maybeSingle();

    if (inviteError) {
      return c.json({ error: inviteError.message }, 500);
    }

    if (!invite) {
      const { data: setupInvite, error: setupInviteError } = await supabase
        .from("client_invites")
        .select("id, invite_type, is_pending, cancelled_at, used_at, setup_completed_at")
        .eq("id", inviteId)
        .eq("tablo_id", tabloId)
        .maybeSingle();

      if (setupInviteError) {
        return c.json({ error: setupInviteError.message }, 500);
      }

      if (!setupInvite) {
        return c.json({ error: "Invite not found" }, 404);
      }

      if (
        setupInvite.invite_type !== "setup" ||
        !setupInvite.is_pending ||
        setupInvite.cancelled_at ||
        setupInvite.used_at ||
        setupInvite.setup_completed_at
      ) {
        return c.json({ error: "Invite is no longer pending" }, 400);
      }

      const { error: cancelSetupInviteError } = await supabase
        .from("client_invites")
        .update({
          cancelled_at: new Date().toISOString(),
          is_pending: false,
        })
        .eq("id", inviteId)
        .eq("tablo_id", tabloId)
        .eq("is_pending", true);

      if (cancelSetupInviteError) {
        return c.json({ error: cancelSetupInviteError.message }, 500);
      }

      return c.json({ success: true });
    }

    if (invite.purpose !== "invite" || invite.consumed_at) {
      return c.json({ error: "Invite is no longer pending" }, 400);
    }

    const cancelledAt = new Date().toISOString();
    const { error: cancelError } = await supabase
      .from("client_magic_links")
      .update({ consumed_at: cancelledAt })
      .eq("id", inviteId)
      .eq("tablo_id", tabloId);

    if (cancelError) {
      return c.json({ error: cancelError.message }, 500);
    }

    const revokeResult = await revokeClientAccess(supabase, {
      clientId: invite.client_id,
      tabloId,
    });

    if (!revokeResult.success) {
      return c.json({ error: revokeResult.error ?? "Failed to revoke client access" }, 500);
    }

    return c.json({ success: true });
  });

export const getClientInvitesRouter = (config: {
  clientsUrl: string;
  jwtSecret: string;
  ttlMinutes: number;
}) => {
  const router = new Hono<AuthEnv>();
  const middlewareManager = MiddlewareManager.getInstance();

  router.post("/:tabloId", ...createClientInvite(middlewareManager, config));
  router.get("/:tabloId/pending", ...getPendingClientInvites(middlewareManager));
  router.delete("/:tabloId/:inviteId", ...cancelClientInvite(middlewareManager));

  return router;
};

export const getPublicClientInvitesRouter = () => {
  const router = new Hono<BaseEnv>();

  router.get("/setup/:token", ...getSetupInvite());
  router.post("/setup/:token", ...completeSetupInvite());

  return router;
};
