import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import type { Tables, TabloFolder, TabloFoldersMetadata } from "@xtablo/shared-types";
import { Hono } from "hono";
import { createFactory, createMiddleware } from "hono/factory";
import { clientHasTabloAccess, getActiveClientAccessTabloIds } from "../helpers/clientAccounts.js";
import { getTabloFileNames } from "../helpers/helpers.js";
import { MiddlewareManager } from "../middlewares/middleware.js";
import type { ClientEnv } from "../types/app.types.js";

const factory = createFactory<ClientEnv>();

const FOLDERS_METADATA_FILE = ".tablo-folders.json";
const CACHE_TTL_MS = 15_000;

type CacheEntry<T> = {
  value: T;
  expiresAt: number;
};

const fileNamesCache = new Map<string, CacheEntry<string[]>>();
const foldersCache = new Map<string, CacheEntry<TabloFoldersMetadata>>();

const getCachedValue = <T>(entry: CacheEntry<T> | undefined): T | null => {
  if (!entry) return null;
  if (Date.now() >= entry.expiresAt) return null;
  return entry.value;
};

const setCacheValue = <T>(map: Map<string, CacheEntry<T>>, key: string, value: T) => {
  map.set(key, {
    value,
    expiresAt: Date.now() + CACHE_TTL_MS,
  });
};

const getCachedTabloFileNames = async (
  s3_client: ClientEnv["Variables"]["s3_client"],
  tabloId: string
): Promise<string[]> => {
  const cached = getCachedValue(fileNamesCache.get(tabloId));
  if (cached) {
    return cached;
  }

  const fileNames = (await getTabloFileNames(s3_client, tabloId)) || [];
  setCacheValue(fileNamesCache, tabloId, fileNames);
  return fileNames;
};

const getFolderMetadata = async (
  s3_client: ClientEnv["Variables"]["s3_client"],
  tabloId: string
): Promise<TabloFoldersMetadata> => {
  const cached = getCachedValue(foldersCache.get(tabloId));
  if (cached) {
    return cached;
  }

  try {
    const response = await s3_client.send(
      new GetObjectCommand({
        Bucket: "tablo-data",
        Key: `${tabloId}/${FOLDERS_METADATA_FILE}`,
      })
    );

    if (response.Body) {
      const content = await response.Body.transformToString();
      const metadata = JSON.parse(content) as TabloFoldersMetadata;
      setCacheValue(foldersCache, tabloId, metadata);
      return metadata;
    }
  } catch {
    // Missing metadata file means the tablo has no folders yet.
  }

  const emptyMetadata = { folders: [], version: 1 };
  setCacheValue(foldersCache, tabloId, emptyMetadata);
  return emptyMetadata;
};

const saveFolderMetadata = async (
  s3_client: ClientEnv["Variables"]["s3_client"],
  tabloId: string,
  metadata: TabloFoldersMetadata
) => {
  await s3_client.send(
    new PutObjectCommand({
      Bucket: "tablo-data",
      Key: `${tabloId}/${FOLDERS_METADATA_FILE}`,
      Body: JSON.stringify(metadata, null, 2),
      ContentType: "application/json",
    })
  );

  setCacheValue(foldersCache, tabloId, metadata);
};

const mapTabloForClient = (tablo: Tables<"tablos">, clientId: string) => ({
  access_level: "guest",
  color: tablo.color,
  created_at: tablo.created_at,
  deleted_at: tablo.deleted_at,
  id: tablo.id,
  image: tablo.image,
  is_admin: false,
  name: tablo.name,
  position: tablo.position,
  status: tablo.status,
  user_id: clientId,
});

const generateFolderId = () => `folder-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;

const checkClientTabloAccess = createMiddleware<ClientEnv>(async (c, next) => {
  const supabase = c.get("supabase");
  const client = c.get("client");
  const tabloId = c.req.param("tabloId");

  const accessResult = await clientHasTabloAccess(supabase, {
    clientId: client.id,
    tabloId,
  });

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

  if (!accessResult.hasAccess) {
    return c.json({ error: "You are not allowed to access this tablo" }, 403);
  }

  await next();
});

const getClientTablos = factory.createHandlers(async (c) => {
  const supabase = c.get("supabase");
  const client = c.get("client");

  const accessResult = await getActiveClientAccessTabloIds(supabase, client.id);
  if (accessResult.error) {
    return c.json({ error: accessResult.error }, 500);
  }

  if (accessResult.tabloIds.length === 0) {
    return c.json({ tablos: [] });
  }

  const { data, error } = await supabase
    .from("tablos")
    .select("*")
    .in("id", accessResult.tabloIds)
    .is("deleted_at", null)
    .order("position", { ascending: true });

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

  return c.json({
    tablos: (data ?? []).map((tablo) => mapTabloForClient(tablo, client.id)),
  });
});

const getClientTablo = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const supabase = c.get("supabase");
  const client = c.get("client");
  const tabloId = c.req.param("tabloId");

  const { data, error } = await supabase
    .from("tablos")
    .select("*")
    .eq("id", tabloId)
    .is("deleted_at", null)
    .maybeSingle();

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

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

  return c.json({ tablo: mapTabloForClient(data, client.id) });
});

const getClientTasks = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const supabase = c.get("supabase");
  const tabloId = c.req.param("tabloId");

  const { data, error } = await supabase
    .from("tasks_with_assignee")
    .select("*")
    .eq("tablo_id", tabloId)
    .eq("is_parent", false)
    .order("updated_at", { ascending: false });

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

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

const getClientEtapes = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const supabase = c.get("supabase");
  const tabloId = c.req.param("tabloId");

  const { data, error } = await supabase
    .from("tasks")
    .select("*")
    .eq("tablo_id", tabloId)
    .eq("is_parent", true)
    .order("position", { ascending: true });

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

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

const getClientEvents = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const supabase = c.get("supabase");
  const tabloId = c.req.param("tabloId");

  const { data, error } = await supabase
    .from("events_and_tablos")
    .select("*")
    .eq("tablo_id", tabloId)
    .order("start_date", { ascending: false });

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

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

const getClientMembers = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const supabase = c.get("supabase");
  const tabloId = c.req.param("tabloId");

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

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

  return c.json({
    members: (data ?? [])
      .map((member) => {
        const profile = Array.isArray(member.profiles) ? member.profiles[0] : member.profiles;

        if (!profile) {
          return null;
        }

        return {
          ...profile,
          email: profile.email,
          is_admin: member.is_admin,
        };
      })
      .filter(Boolean),
  });
});

const createClientTask = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const supabase = c.get("supabase");
  const tabloId = c.req.param("tabloId");
  const body = await c.req.json();

  const payload = {
    assignee_id: body.assignee_id ?? null,
    description: body.description ?? null,
    due_date: body.due_date ?? null,
    is_parent: body.is_parent ?? false,
    parent_task_id: body.parent_task_id ?? null,
    position: body.position ?? 0,
    status: body.status ?? "todo",
    tablo_id: tabloId,
    title: body.title,
  };

  const { data, error } = await supabase.from("tasks").insert(payload).select().single();

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

  return c.json({ task: data });
});

const updateClientTask = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const supabase = c.get("supabase");
  const tabloId = c.req.param("tabloId");
  const taskId = c.req.param("taskId");
  const body = await c.req.json();

  const { tablo_id: _ignoredTabloId, ...updates } = body as Record<string, unknown> & {
    tablo_id?: string;
  };

  const { data, error } = await supabase
    .from("tasks")
    .update(updates)
    .eq("id", taskId)
    .eq("tablo_id", tabloId)
    .select()
    .single();

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

  return c.json({ task: data });
});

const deleteClientTask = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const supabase = c.get("supabase");
  const tabloId = c.req.param("tabloId");
  const taskId = c.req.param("taskId");

  const { error } = await supabase.from("tasks").delete().eq("id", taskId).eq("tablo_id", tabloId);

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

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

const getClientTabloFilenames = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const tabloId = c.req.param("tabloId");
  const s3_client = c.get("s3_client");

  try {
    const fileNames = await getCachedTabloFileNames(s3_client, tabloId);
    return c.json({ fileNames });
  } catch (error) {
    console.error("Error fetching client tablo files:", error);
    return c.json({ error: "Failed to fetch tablo files" }, 500);
  }
});

const getClientTabloFile = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const tabloId = c.req.param("tabloId");
  const filePath = c.req.param("path");
  const s3_client = c.get("s3_client");

  if (!filePath) {
    return c.json({ error: "File path is required" }, 400);
  }

  try {
    const response = await s3_client.send(
      new GetObjectCommand({
        Bucket: "tablo-data",
        Key: `${tabloId}/${filePath}`,
      })
    );

    if (!response.Body) {
      return c.json({ error: "File not found" }, 404);
    }

    const content = await response.Body.transformToString();

    return c.json({
      content,
      contentType: response.ContentType,
      fileName: filePath,
      lastModified: response.LastModified,
    });
  } catch (error) {
    console.error("Error fetching client file:", error);
    return c.json({ error: "Failed to fetch file" }, 500);
  }
});

const postClientTabloFile = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const tabloId = c.req.param("tabloId");
  const filePath = c.req.param("path");
  const client = c.get("client");
  const s3_client = c.get("s3_client");

  if (!filePath) {
    return c.json({ error: "File path is required" }, 400);
  }

  try {
    const body = await c.req.json();
    const { content, contentType = "text/plain" } = body;

    if (!content) {
      return c.json({ error: "Content is required" }, 400);
    }

    await s3_client.send(
      new PutObjectCommand({
        Bucket: "tablo-data",
        Key: `${tabloId}/${filePath}`,
        Body: content,
        ContentType: contentType,
        Metadata: {
          "uploaded-by": client.id,
        },
      })
    );

    fileNamesCache.delete(tabloId);

    return c.json({
      fileName: filePath,
      message: "File uploaded successfully",
      tabloId,
    });
  } catch (error) {
    console.error("Error uploading client file:", error);
    return c.json({ error: "Failed to upload file" }, 500);
  }
});

const getClientTabloFolders = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const tabloId = c.req.param("tabloId");
  const s3_client = c.get("s3_client");

  try {
    const metadata = await getFolderMetadata(s3_client, tabloId);
    return c.json({ folders: metadata.folders ?? [] });
  } catch (error) {
    console.error("Error fetching client folders:", error);
    return c.json({ error: "Failed to fetch folders" }, 500);
  }
});

const createClientTabloFolder = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const tabloId = c.req.param("tabloId");
  const client = c.get("client");
  const s3_client = c.get("s3_client");

  try {
    const body = await c.req.json();
    const name = String(body.name || "").trim();
    const description = String(body.description || "").trim();

    if (!name) {
      return c.json({ error: "Folder name is required" }, 400);
    }

    const metadata = await getFolderMetadata(s3_client, tabloId);
    const newFolder: TabloFolder = {
      createdAt: new Date().toISOString(),
      createdBy: client.id,
      description,
      id: generateFolderId(),
      name,
    };

    const nextMetadata = {
      ...metadata,
      folders: [...metadata.folders, newFolder],
      version: metadata.version + 1,
    };

    await saveFolderMetadata(s3_client, tabloId, nextMetadata);

    return c.json({
      folder: newFolder,
      message: "Folder created successfully",
    });
  } catch (error) {
    console.error("Error creating client folder:", error);
    return c.json({ error: "Failed to create folder" }, 500);
  }
});

const updateClientTabloFolder = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const tabloId = c.req.param("tabloId");
  const folderId = c.req.param("folderId");
  const s3_client = c.get("s3_client");

  try {
    const body = await c.req.json();
    const name = String(body.name || "").trim();
    const description = String(body.description || "").trim();

    if (!name) {
      return c.json({ error: "Folder name is required" }, 400);
    }

    const metadata = await getFolderMetadata(s3_client, tabloId);
    const existingFolder = metadata.folders.find((folder) => folder.id === folderId);

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

    const updatedFolder: TabloFolder = {
      ...existingFolder,
      description,
      name,
    };

    const nextMetadata = {
      ...metadata,
      folders: metadata.folders.map((folder) => (folder.id === folderId ? updatedFolder : folder)),
      version: metadata.version + 1,
    };

    await saveFolderMetadata(s3_client, tabloId, nextMetadata);

    return c.json({
      folder: updatedFolder,
      message: "Folder updated successfully",
    });
  } catch (error) {
    console.error("Error updating client folder:", error);
    return c.json({ error: "Failed to update folder" }, 500);
  }
});

const deleteClientTabloFolder = factory.createHandlers(checkClientTabloAccess, async (c) => {
  const tabloId = c.req.param("tabloId");
  const folderId = c.req.param("folderId");
  const s3_client = c.get("s3_client");

  try {
    const metadata = await getFolderMetadata(s3_client, tabloId);
    const folderExists = metadata.folders.some((folder) => folder.id === folderId);

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

    const nextMetadata = {
      ...metadata,
      folders: metadata.folders.filter((folder) => folder.id !== folderId),
      version: metadata.version + 1,
    };

    await saveFolderMetadata(s3_client, tabloId, nextMetadata);

    return c.json({
      message: "Folder deleted successfully",
    });
  } catch (error) {
    console.error("Error deleting client folder:", error);
    return c.json({ error: "Failed to delete folder" }, 500);
  }
});

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

  router.use(middlewareManager.clientAuth);

  router.get("/tablos", ...getClientTablos);
  router.get("/tablos/:tabloId", ...getClientTablo);
  router.get("/tablos/:tabloId/tasks", ...getClientTasks);
  router.get("/tablos/:tabloId/etapes", ...getClientEtapes);
  router.get("/tablos/:tabloId/events", ...getClientEvents);
  router.get("/tablos/:tabloId/members", ...getClientMembers);
  router.post("/tablos/:tabloId/tasks", ...createClientTask);
  router.patch("/tablos/:tabloId/tasks/:taskId", ...updateClientTask);
  router.delete("/tablos/:tabloId/tasks/:taskId", ...deleteClientTask);
  router.get("/tablos/:tabloId/files", ...getClientTabloFilenames);
  router.get("/tablos/:tabloId/folders", ...getClientTabloFolders);
  router.post("/tablos/:tabloId/folders", ...createClientTabloFolder);
  router.put("/tablos/:tabloId/folders/:folderId", ...updateClientTabloFolder);
  router.delete("/tablos/:tabloId/folders/:folderId", ...deleteClientTabloFolder);
  router.get("/tablos/:tabloId/file/:path{.+}", ...getClientTabloFile);
  router.post("/tablos/:tabloId/file/:path{.+}", ...postClientTabloFile);

  return router;
};
