import type { EventInsertInTablo, Tables, TabloInsert } from "@xtablo/shared-types";
import { Hono } from "hono";
import { createFactory } from "hono/factory";
import type { AppConfig } from "../config.js";
import {
  checkTabloAdmin,
  createInvitedUser,
  verifyTabloLimitForUser,
  writeCalendarFileToR2,
} from "../helpers/helpers.js";
import { generateToken } from "../helpers/token.js";
import { MiddlewareManager } from "../middlewares/middleware.js";
import type { AuthEnv } from "../types/app.types.js";

type PostTablo = Omit<TabloInsert, "owner_id" | "organization_id"> & {
  events?: EventInsertInTablo[];
};

const factory = createFactory<AuthEnv>();

const isAlreadyMemberError = (error: unknown): boolean => {
  if (!error) return false;
  const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
  return (
    message.includes("already a member") ||
    message.includes("already member") ||
    message.includes("member already exists")
  );
};

const upsertStreamUserFromProfile = async (
  supabase: AuthEnv["Variables"]["supabase"],
  streamServerClient: AuthEnv["Variables"]["streamServerClient"],
  userId: string
) => {
  const { data: profile } = await supabase
    .from("profiles")
    .select("name")
    .eq("id", userId)
    .maybeSingle();

  await streamServerClient.upsertUser({
    id: userId,
    name: profile?.name ?? "",
    language: "fr",
  });
};

const ensureTabloChannelMember = async (
  supabase: AuthEnv["Variables"]["supabase"],
  streamServerClient: AuthEnv["Variables"]["streamServerClient"],
  tabloId: string,
  userId: string
) => {
  const channel = streamServerClient.channel("messaging", tabloId);

  try {
    await channel.addMembers([userId]);
    return;
  } catch (error) {
    if (isAlreadyMemberError(error)) {
      return;
    }
  }

  const { data: tablo } = await supabase
    .from("tablos")
    .select("name, owner_id")
    .eq("id", tabloId)
    .maybeSingle();

  const { data: accessRows } = await supabase
    .from("tablo_access")
    .select("user_id")
    .eq("tablo_id", tabloId)
    .eq("is_active", true);

  const members = Array.from(new Set((accessRows || []).map((row) => row.user_id).concat(userId)));

  const channelToCreate = streamServerClient.channel("messaging", tabloId, {
    // @ts-ignore
    name: tablo?.name ?? "Tablo",
    created_by_id: tablo?.owner_id ?? userId,
    members,
  });

  try {
    await channelToCreate.create();
  } catch (error) {
    if (isAlreadyMemberError(error)) {
      return;
    }

    await channel.addMembers([userId]);
  }
};

const createTablo = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, verifyTabloLimitForUser, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const data = await c.req.json();

    const typedPayload = data as PostTablo;

    const { data: profile, error: profileError } = await supabase
      .from("profiles")
      .select("organization_id")
      .eq("id", user.id)
      .single();

    if (profileError || !profile?.organization_id) {
      return c.json({ error: "Failed to resolve your organization" }, 500);
    }

    const { data: insertedTablo, error } = await supabase
      .from("tablos")
      .insert({
        ...typedPayload,
        owner_id: user.id,
        organization_id: profile.organization_id,
        events: undefined,
      })
      .select()
      .single();

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

    const tabloData = insertedTablo as Tables<"tablos">;

    const { data: organizationMembers, error: membersError } = await supabase
      .from("profiles")
      .select("id")
      .eq("organization_id", profile.organization_id);

    if (membersError) {
      return c.json({ error: "Failed to load organization members" }, 500);
    }

    const channelMembers = Array.from(
      new Set((organizationMembers || []).map((member) => member.id).concat(user.id))
    );

    const streamServerClient = c.get("streamServerClient");
    const channel = streamServerClient.channel("messaging", tabloData.id, {
      // @ts-ignore
      name: tabloData.name,
      created_by_id: user.id,
      members: channelMembers,
    });
    await channel.create();

    if (typedPayload.events) {
      const eventsToInsert = typedPayload.events.map((event) => ({
        ...event,
        tablo_id: tabloData.id,
        created_by: user.id,
      }));

      await supabase.from("events").insert(eventsToInsert);
    }
    return c.json({ message: "Tablo created successfully", tablo: tabloData });
  });

const updateTablo = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const streamServerClient = c.get("streamServerClient");
    const data = await c.req.json();

    const { id, ...tablo } = data;

    const { data: access, error: accessError } = await supabase
      .from("tablo_access")
      .select("is_admin")
      .eq("tablo_id", id)
      .eq("user_id", user.id)
      .eq("is_active", true)
      .single();

    if (accessError || !access || !access.is_admin) {
      return c.json({ error: "You are not authorized to update this tablo" }, 403);
    }

    const { data: update, error } = await supabase
      .from("tablos")
      .update(tablo)
      .eq("id", id)
      .select()
      .single();

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

    const updatedTablo = update as Tables<"tablos">;
    const isUpdatingName = tablo.name !== undefined;

    if (isUpdatingName) {
      const channel = streamServerClient.channel("messaging", updatedTablo.id);
      try {
        await channel.update({
          // @ts-ignore
          name: updatedTablo.name,
        });
      } catch (error) {
        console.error("error updating channel", error);
      }
    }

    return c.json({ message: "Tablo updated successfully" });
  });

const deleteTablo = factory.createHandlers(async (c) => {
  const user = c.get("user");
  const supabase = c.get("supabase");
  const streamServerClient = c.get("streamServerClient");
  const data = await c.req.json();

  const { id } = data;

  // Verify that the user has admin access to this tablo
  const { data: tabloAccess, error: accessError } = await supabase
    .from("tablo_access")
    .select("is_admin")
    .eq("tablo_id", id)
    .eq("user_id", user.id)
    .eq("is_active", true)
    .single();

  if (accessError || !tabloAccess || !tabloAccess.is_admin) {
    return c.json({ error: "You are not authorized to delete this tablo" }, 403);
  }

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

  const { error: tasksSoftDeleteError } = await supabase
    .from("tasks")
    .update({ deleted_at: deletedAt })
    .eq("tablo_id", id)
    .is("deleted_at", null);

  if (tasksSoftDeleteError) {
    // Backward compatibility for environments where tasks.deleted_at is not migrated yet.
    const isMissingDeletedAtColumn =
      tasksSoftDeleteError.code === "42703" ||
      tasksSoftDeleteError.message?.toLowerCase().includes("deleted_at");

    if (isMissingDeletedAtColumn) {
      const { error: tasksDeleteError } = await supabase.from("tasks").delete().eq("tablo_id", id);
      if (tasksDeleteError) {
        return c.json({ error: tasksDeleteError.message }, 500);
      }
    } else {
      return c.json({ error: tasksSoftDeleteError.message }, 500);
    }
  }

  const { error } = await supabase.from("tablos").update({ deleted_at: deletedAt }).eq("id", id);

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

  const channel = streamServerClient.channel("messaging", id);
  try {
    await channel.delete();
  } catch (error) {
    console.error("error deleting channel", error);
  }

  return c.json({ message: "Tablo deleted successfully" });
});

const inviteToTablo = (
  config: AppConfig,
  middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>
) =>
  factory.createHandlers(middlewareManager.regularUserCheck, checkTabloAdmin, async (c) => {
    const transporter = c.get("transporter");
    const sender = c.get("user");
    const supabase = c.get("supabase");
    const streamServerClient = c.get("streamServerClient");

    const tabloId = c.req.param("tabloId");
    const { email: recipientmail } = await c.req.json();
    const recipientEmail = String(recipientmail || "")
      .trim()
      .toLowerCase();

    if (sender.email?.toLowerCase() === recipientEmail) {
      return c.json({ error: "You cannot invite yourself" }, 400);
    }

    // Get tablo name
    const { data: tablo, error: tabloError } = await supabase
      .from("tablos")
      .select("name")
      .eq("id", tabloId)
      .maybeSingle();

    if (tabloError || !tablo) {
      return c.json({ error: "Tablo not found" }, 404);
    }

    const token = generateToken();

    const { data: introConfigData, error: introError } = await supabase
      .from("user_introductions")
      .select("config")
      .eq("user_id", sender.id)
      .maybeSingle();

    if (introError) {
      return c.json({ error: introError.message }, 500);
    }
    const introEmail = introConfigData?.config?.intro_email;

    const { error } = await supabase.from("tablo_invites").insert({
      invited_email: recipientEmail,
      tablo_id: tabloId,
      invited_by: sender.id,
      invite_token: token,
      is_pending: true,
    });

    if (error) {
      // Check if this is a duplicate invite error
      if (error.code === "23505") {
        return c.json({ error: "User has already been invited to this tablo" }, 409);
      }
      return c.json({ error: error.message }, 500);
    }

    // Get user from recipient email
    const { data: recipientUser, error: recipientError } = await supabase
      .from("profiles")
      .select("id")
      .eq("email", recipientEmail)
      .maybeSingle();

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

    if (!recipientUser) {
      // Create a new invited user and add them to the tablo
      const result = await createInvitedUser(
        supabase,
        streamServerClient,
        transporter,
        recipientEmail,
        sender.email,
        { isTemporary: true }
      );

      if (!result.success) {
        return c.json({ error: result.error }, 500);
      }

      // Add the user to the tablo
      const { error: tabloAccessError } = await supabase.from("tablo_access").insert({
        tablo_id: tabloId,
        user_id: result.userId,
        granted_by: sender.id,
        is_active: true,
        // ** IMPORTANT **
        is_admin: false,
        // -------------
      });

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

      try {
        await ensureTabloChannelMember(supabase, streamServerClient, tabloId, result.userId);
      } catch (streamError) {
        console.error("error adding temporary invited user to channel", streamError);
        return c.json({ error: "Failed to sync chat access for invited user" }, 500);
      }

      return c.json({
        message: "User created and invite sent successfully",
      });
    }

    // Check if the user already has access to the tablo
    const { data: existingAccess, error: existingAccessError } = await supabase
      .from("tablo_access")
      .select("id")
      .eq("tablo_id", tabloId)
      .eq("user_id", recipientUser.id)
      .maybeSingle();

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

    if (existingAccess) {
      return c.json({ message: "User already has access to this tablo" }, 400);
    }

    // Let the user know that they have been invited to the tablo
    await transporter.sendMail({
      from: `${sender.email} via XTablo <noreply@xtablo.com>`,
      to: recipientEmail,
      subject: "Vous avez été invité à un tablo",
      html: `
${introEmail ? `<p>${introEmail}</p>` : ""}
<p>Cliquez sur <a href="${
        config.XTABLO_URL
      }/join-tablo?tablo_name=${encodeURIComponent(tablo.name)}&token=${encodeURIComponent(
        token
      )}">ce lien</a> pour accepter l'invitation.</p>
<br>
<p style="color: #666; font-size: 14px; margin-top: 30px;">
  Cordialement,<br>
  L'équipe XTablo
</p>
    `,
    });

    return c.json({
      message: "Invite sent successfully",
    });
  });

const cancelPendingInvite = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const streamServerClient = c.get("streamServerClient");
    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("tablo_invites")
      .select("id, invited_email, is_pending")
      .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 isInvitee = invite.invited_email?.toLowerCase() === user.email?.toLowerCase();

    const { data: adminAccess, error: adminAccessError } = await supabase
      .from("tablo_access")
      .select("id")
      .eq("tablo_id", tabloId)
      .eq("user_id", user.id)
      .eq("is_active", true)
      .eq("is_admin", true)
      .maybeSingle();

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

    const isAdmin = !!adminAccess;

    if (!isInvitee && !isAdmin) {
      return c.json({ error: "You are not authorized to cancel this invite" }, 403);
    }

    const { error: cancelError } = await supabase
      .from("tablo_invites")
      .update({ is_pending: false })
      .eq("id", inviteId)
      .eq("tablo_id", tabloId);

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

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

    // Temporary invitees are pre-added to tablo_access. Revoke this access when invite is cancelled.
    if (invitedProfile?.id && invitedProfile.is_temporary) {
      const { error: revokeAccessError } = await supabase
        .from("tablo_access")
        .update({ is_active: false })
        .eq("tablo_id", tabloId)
        .eq("user_id", invitedProfile.id);

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

      try {
        const channel = streamServerClient.channel("messaging", tabloId);
        await channel.removeMembers([invitedProfile.id]);
      } catch (error) {
        console.error("error removing cancelled invitee from channel", error);
      }
    }

    return c.json({ message: "Invite cancelled successfully" });
  });

const getPendingInvitesForCurrentUser = (
  middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>
) =>
  factory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");

    const { data: pendingInvites, error: pendingInvitesError } = await supabase
      .from("tablo_invites")
      .select("id, tablo_id, created_at")
      .eq("invited_email", user.email?.toLowerCase())
      .eq("is_pending", true)
      .order("created_at", { ascending: false });

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

    const tabloIds = Array.from(new Set((pendingInvites || []).map((invite) => invite.tablo_id)));
    let tablosById = new Map<string, string>();

    if (tabloIds.length > 0) {
      const { data: tablos, error: tablosError } = await supabase
        .from("tablos")
        .select("id, name")
        .in("id", tabloIds);

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

      tablosById = new Map((tablos || []).map((tablo) => [tablo.id, tablo.name]));
    }

    return c.json({
      invites: (pendingInvites || []).map((invite) => ({
        id: invite.id,
        tablo_id: invite.tablo_id,
        tablo_name: tablosById.get(invite.tablo_id) || "Tablo",
        created_at: invite.created_at,
      })),
    });
  });

const acceptInviteById = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const streamServerClient = c.get("streamServerClient");
    const inviteId = Number(c.req.param("inviteId"));

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

    const { data: inviteData, error: inviteError } = await supabase
      .from("tablo_invites")
      .select("id, tablo_id, invited_by, invited_email, is_pending")
      .eq("id", inviteId)
      .maybeSingle();

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

    if (!inviteData || !inviteData.is_pending) {
      return c.json({ error: "Invite not found or no longer pending" }, 404);
    }

    if (inviteData.invited_email?.toLowerCase() !== user.email?.toLowerCase()) {
      return c.json({ error: "You are not authorized to accept this invite" }, 403);
    }

    try {
      await upsertStreamUserFromProfile(supabase, streamServerClient, user.id);
    } catch (error) {
      console.error("error upserting joining user to stream", error);
      return c.json({ error: "Failed to provision chat user" }, 500);
    }

    const { error: tabloAccessError } = await supabase.from("tablo_access").insert({
      tablo_id: inviteData.tablo_id,
      user_id: user.id,
      is_admin: false,
      is_active: true,
      granted_by: inviteData.invited_by,
    });

    if (tabloAccessError) {
      if (tabloAccessError.code !== "23505") {
        return c.json({ error: tabloAccessError.message }, 500);
      }
    }

    await supabase.from("tablo_invites").update({ is_pending: false }).eq("id", inviteData.id);

    try {
      await ensureTabloChannelMember(supabase, streamServerClient, inviteData.tablo_id, user.id);
    } catch (error) {
      console.error("error adding member to channel", error);
      return c.json({ error: "Failed to sync chat access for this tablo" }, 500);
    }

    return c.json({ tablo_id: inviteData.tablo_id });
  });

const joinTablo = factory.createHandlers(async (c) => {
  const { token } = await c.req.json();

  const joiner = c.get("user");
  const supabase = c.get("supabase");
  const streamServerClient = c.get("streamServerClient");

  const { data: inviteData, error } = await supabase
    .from("tablo_invites")
    .select("id, tablo_id, invited_by")
    .eq("invite_token", token)
    .eq("invited_email", joiner.email)
    .eq("is_pending", true)
    .maybeSingle();

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

  if (!inviteData) {
    return c.json({ error: "Invalid token or email" }, 400);
  }

  const { id: invite_id, tablo_id, invited_by } = inviteData;

  try {
    await upsertStreamUserFromProfile(supabase, streamServerClient, joiner.id);
  } catch (error) {
    console.error("error upserting joining user to stream", error);
    return c.json({ error: "Failed to provision chat user" }, 500);
  }

  const { error: tabloAccessError } = await supabase.from("tablo_access").insert({
    tablo_id,
    user_id: joiner.id,
    // ** IMPORTANT **
    is_admin: false,
    // -------------
    is_active: true,
    granted_by: invited_by,
  });

  if (tabloAccessError) {
    console.error("tabloAccessError", tabloAccessError);

    // If user already has access, continue to sync invite + chat membership.
    if (tabloAccessError.code !== "23505") {
      return c.json({ error: tabloAccessError.message }, 500);
    }
  }

  // Mark invite as accepted instead of deleting (maintains audit trail)
  await supabase.from("tablo_invites").update({ is_pending: false }).eq("id", invite_id);

  try {
    await ensureTabloChannelMember(supabase, streamServerClient, tablo_id, joiner.id);
  } catch (error) {
    console.error("error adding member to channel", error);
    return c.json({ error: "Failed to sync chat access for this tablo" }, 500);
  }

  return c.json({ tablo_id });
});

const getTabloMembers = factory.createHandlers(async (c) => {
  const user = c.get("user");
  const supabase = c.get("supabase");
  const tablo_id = c.req.param("tablo_id");

  const { data: tabloData, error: tabloError } = await supabase
    .from("user_tablos")
    .select("*")
    .eq("id", tablo_id)
    .eq("user_id", user.id);

  if (!tabloData || tabloData.length === 0) {
    return c.json({ error: "You are not a member of this tablo" }, 403);
  }

  if (tabloError) {
    return c.json({ error: "Internal server error" }, 500);
  }

  const { data, error } = await supabase
    .from("tablo_access")
    .select("is_admin, profiles(id, name, email, avatar_url)")
    .eq("tablo_id", tablo_id)
    .eq("is_active", true);

  const rows = data as unknown as {
    is_admin: boolean;
    profiles: {
      id: string;
      name: string;
      email: string;
      avatar_url: string | null;
    };
  }[];

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

  return c.json({
    members: rows.map((member) => ({
      ...member.profiles,
      is_admin: member.is_admin,
      email: member.profiles.email,
      avatar_url: member.profiles.avatar_url,
    })),
  });
});

const leaveTablo = factory.createHandlers(async (c) => {
  const user = c.get("user");
  const supabase = c.get("supabase");
  const streamServerClient = c.get("streamServerClient");
  const { tablo_id } = await c.req.json();

  const channel = streamServerClient.channel("messaging", tablo_id);
  await channel.removeMembers([user.id]);

  const { error } = await supabase
    .from("tablo_access")
    .update({ is_active: false })
    .eq("tablo_id", tablo_id)
    .eq("user_id", user.id);

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

  return c.json({ message: "Tablo left successfully" });
});

const generateWebcalUrl = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const s3_client = c.get("s3_client");

    const { tablo_id } = await c.req.json();

    if (tablo_id === null) {
      return c.json({ error: "All tablos are not supported" }, 400);
    }

    const { data: tabloData, error: tabloError } = await supabase
      .from("tablos")
      .select("name")
      .eq("id", tablo_id)
      .single();

    if (tabloError || !tabloData) {
      return c.json({ error: "Tablo not found" }, 404);
    }

    const tabloName = tabloData.name.replace(/ /g, "_");

    const { data: accessData, error: accessError } = await supabase
      .from("user_tablos")
      .select("id")
      .eq("id", tablo_id)
      .eq("user_id", user.id)
      .single();

    if (accessError || !accessData) {
      return c.json({ error: "Access denied to this tablo" }, 403);
    }

    const { data: subscriptionData } = await supabase
      .from("calendar_subscriptions")
      .select("*")
      .eq("tablo_id", tablo_id)
      .single();

    // if (subscriptionError || !subscriptionData) {
    //   return c.json({ error: "Subscription already exists" }, 400);
    // }

    if (subscriptionData) {
      const token = subscriptionData.token;
      const httpUrl = `https://calendar.xtablo.com/${token}/${tabloName}.ics`;

      return c.json({
        webcal_url: null,
        http_url: httpUrl,
      });
    }

    const token = generateToken();

    const { error } = await supabase.from("calendar_subscriptions").insert({
      tablo_id: tablo_id,
      token: token,
    });

    if (error) {
      return c.json({ error: "Failed to generate token" }, 500);
    }

    try {
      await writeCalendarFileToR2(s3_client, supabase, {
        token,
        tabloName,
        tablo_id,
      });
    } catch (error) {
      console.error("error writing calendar file to R2", error);
      return c.json({ error: "Failed to write calendar file to R2" }, 500);
    }

    // Return the webcal URL
    // const webcalUrl = `webcal://${
    //   c.req.header("host") || "localhost:3000"
    // }/api/v1/tablos/webcal/${tablo_id}/${token}`;
    const httpUrl = `https://calendar.xtablo.com/${token}/${tabloName}.ics`;

    return c.json({
      webcal_url: null,
      http_url: httpUrl,
    });
  });

const canCreateTablo = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, verifyTabloLimitForUser, async (c) => {
    return c.json({ canCreate: true });
  });

export const getTabloRouter = (config: AppConfig) => {
  const tabloRouter = new Hono();
  const middlewareManager = MiddlewareManager.getInstance();

  tabloRouter.use(middlewareManager.supabase);
  tabloRouter.use(middlewareManager.auth);
  tabloRouter.use(middlewareManager.streamChat);

  tabloRouter.post("/create", ...createTablo(middlewareManager));
  tabloRouter.patch("/update", ...updateTablo(middlewareManager));
  tabloRouter.delete("/delete", ...deleteTablo);
  tabloRouter.get("/invites/pending", ...getPendingInvitesForCurrentUser(middlewareManager));
  tabloRouter.post("/invites/:inviteId/accept", ...acceptInviteById(middlewareManager));
  tabloRouter.post("/invite/:tabloId", ...inviteToTablo(config, middlewareManager));
  tabloRouter.delete("/invite/:tabloId/:inviteId", ...cancelPendingInvite(middlewareManager));
  tabloRouter.post("/join", ...joinTablo);
  tabloRouter.get("/members/:tablo_id", ...getTabloMembers);
  tabloRouter.post("/leave", ...leaveTablo);
  tabloRouter.post("/webcal/generate-url", ...generateWebcalUrl(middlewareManager));
  tabloRouter.get("/can-create-tablo", ...canCreateTablo(middlewareManager));

  return tabloRouter;
};
