import { Hono } from "hono";
import { createFactory } from "hono/factory";
import {
  checkTabloAdmin,
  createClientSetupInvite,
  ensureClientTabloAccess,
  findOrCreateClientAccount,
} from "../helpers/helpers.js";
import { generateToken } from "../helpers/token.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 CLIENT_INVITE_EXPIRY_HOURS = 72;

const getClientsUrl = () => process.env.CLIENTS_URL || "https://clients.xtablo.com";

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

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>Créez votre mot de passe via le lien ci-dessous pour accéder à votre espace client :</p>
      <p><a href="${input.setupUrl}">Configurer mon mot de passe</a></p>
      <p>Ce lien expire dans ${CLIENT_INVITE_EXPIRY_HOURS} heures et ne peut être utilisé qu'une seule fois.</p>
    `,
  });
};

const sendAccessNotificationEmail = async (
  transporter: BaseEnv["Variables"]["transporter"],
  input: { email: string; tabloUrl: string }
) => {
  await transporter.sendMail({
    from: "Xtablo <noreply@xtablo.com>",
    to: input.email,
    subject: "Vous avez maintenant accès à un nouveau tablo",
    html: `
      <h2>Vous avez maintenant accès à un tablo</h2>
      <p>Bonjour,</p>
      <p>Votre accès a été ajouté. Utilisez le lien ci-dessous pour ouvrir directement le tablo :</p>
      <p><a href="${input.tabloUrl}">Ouvrir le tablo</a></p>
      <p>Si vous n'êtes pas connecté, vous serez redirigé vers la page de connexion.</p>
    `,
  });
};

/** POST /:tabloId — Create a client invite (admin only) */
const createClientInvite = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  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 = String(body.email || "")
      .trim()
      .toLowerCase();

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

    const accountResult = await findOrCreateClientAccount(supabase, rawEmail);
    if ("error" in accountResult) {
      const errorMessage = accountResult.error;
      if (errorMessage.includes("already belongs")) {
        return c.json({ error: errorMessage }, 409);
      }
      return c.json({ error: errorMessage }, 500);
    }

    const accessResult = await ensureClientTabloAccess(
      supabase,
      tabloId,
      accountResult.account.userId,
      user.id
    );
    if (!accessResult.success) {
      return c.json({ error: accessResult.error ?? "Failed to grant client access" }, 500);
    }

    const clientsUrl = getClientsUrl();

    if (accountResult.account.client_onboarded_at) {
      try {
        await sendAccessNotificationEmail(transporter, {
          email: rawEmail,
          tabloUrl: `${clientsUrl}/tablo/${tabloId}`,
        });
      } catch (emailError) {
        console.error("Failed to send client access notification email:", emailError);
      }

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

    const token = generateToken();
    const expiresAt = new Date(
      Date.now() + CLIENT_INVITE_EXPIRY_HOURS * 60 * 60 * 1000
    ).toISOString();

    const inviteResult = await createClientSetupInvite(supabase, {
      tabloId,
      invitedEmail: rawEmail,
      invitedBy: user.id,
      token,
      expiresAt,
    });

    if (!inviteResult.success) {
      if (inviteResult.error?.includes("idx_client_invites_pending_setup_email_tablo")) {
        return c.json({ error: "A pending invite already exists for this email and tablo" }, 409);
      }
      return c.json({ error: inviteResult.error ?? "Failed to create setup invite" }, 500);
    }

    try {
      await sendSetupEmail(transporter, {
        email: rawEmail,
        setupUrl: `${clientsUrl}/set-password?token=${encodeURIComponent(token)}`,
      });
    } catch (emailError) {
      console.error("Failed to send client setup 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_invites")
      .select("id, invited_email, expires_at, is_pending, created_at, invite_type")
      .eq("tablo_id", tabloId)
      .eq("invite_type", "setup")
      .eq("is_pending", true)
      .order("created_at", { ascending: false });

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

    return c.json({ invites: invites ?? [] });
  });

/** 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_invites")
      .select("id, invited_email, is_pending, invite_type")
      .eq("id", inviteId)
      .eq("tablo_id", tabloId)
      .maybeSingle();

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

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

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

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

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

    if (invite.invited_email) {
      const { data: clientProfile } = await supabase
        .from("profiles")
        .select("id")
        .eq("email", invite.invited_email)
        .maybeSingle();

      if (clientProfile?.id) {
        await supabase
          .from("tablo_access")
          .update({ is_active: false })
          .eq("tablo_id", tabloId)
          .eq("user_id", clientProfile.id);
      }
    }

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

export const getClientInvitesRouter = () => {
  const router = new Hono<AuthEnv>();
  const middlewareManager = MiddlewareManager.getInstance();

  router.post("/:tabloId", ...createClientInvite(middlewareManager));
  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;
};
