import {
  DeleteObjectCommand,
  GetObjectCommand,
  HeadObjectCommand,
  PutObjectCommand,
  S3Client,
} from "@aws-sdk/client-s3";
import type { TabloFolder, TabloFoldersMetadata } from "@xtablo/shared-types";
import { Hono } from "hono";
import { createFactory } from "hono/factory";
import { checkTabloAdmin, checkTabloMember, getTabloFileNames } from "../helpers/helpers.js";
import { MiddlewareManager } from "../middlewares/middleware.js";
import type { AuthEnv } from "../types/app.types.js";

const factory = createFactory<AuthEnv>();

// Metadata file name for folders
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>>();

export const clearTabloDataCachesForTests = () => {
  fileNamesCache.clear();
  foldersCache.clear();
};

// Helper to generate unique folder IDs
const generateFolderId = () => `folder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;

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: S3Client, 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;
};

// ============================================
// FILE ENDPOINTS
// ============================================

const getTabloFilenames = factory.createHandlers(checkTabloMember, 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 tablo files:", error);
    return c.json({ error: "Failed to fetch tablo files" }, 500);
  }
});

// Returns file names for all tablos the authenticated user has access to, in one request
const getAllTablosFilenames = factory.createHandlers(async (c) => {
  const supabase = c.get("supabase");
  const user = c.get("user");
  const s3_client = c.get("s3_client");

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

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

    const tabloIds = (tabloAccess ?? []).map((row: { tablo_id: string }) => row.tablo_id);

    const results = await Promise.all(
      tabloIds.map(async (tabloId: string) => {
        const fileNames = await getCachedTabloFileNames(s3_client, tabloId);
        return { tabloId, fileNames };
      })
    );

    return c.json({ tablos: results });
  } catch (error) {
    console.error("Error fetching all tablo files:", error);
    return c.json({ error: "Failed to fetch all tablo files" }, 500);
  }
});

const getTabloFile = factory.createHandlers(checkTabloMember, async (c) => {
  const tabloId = c.req.param("tabloId");
  // Get the file path - supports both wildcard (*) and named parameter (:fileName)
  const filePath = c.req.param("path") || c.req.param("fileName");

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

  const s3_client = c.get("s3_client");

  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({
      fileName: filePath,
      content,
      contentType: response.ContentType,
      lastModified: response.LastModified,
    });
  } catch (error) {
    console.error("Error fetching file:", error);
    return c.json({ error: "Failed to fetch file" }, 500);
  }
});

const postTabloFile = factory.createHandlers(checkTabloMember, async (c) => {
  const tabloId = c.req.param("tabloId");
  const user = c.get("user");
  // Get the file path - supports both wildcard (*) and named parameter (:fileName)
  const filePath = c.req.param("path") || c.req.param("fileName");

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

  const s3_client = c.get("s3_client");

  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": user.id,
        },
      })
    );
    fileNamesCache.delete(tabloId);

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

const deleteTabloFile = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(checkTabloMember, async (c) => {
    const tabloId = c.req.param("tabloId");
    const user = c.get("user");
    const supabase = c.get("supabase");
    // Get the file path - supports both wildcard (*) and named parameter (:fileName)
    const filePath = c.req.param("path") || c.req.param("fileName");

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

    const s3_client = c.get("s3_client");

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

      if (accessError) {
        return c.json({ error: "Failed to verify access rights" }, 500);
      }

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

      if (!access.is_admin) {
        try {
          const headResponse = await s3_client.send(
            new HeadObjectCommand({
              Bucket: "tablo-data",
              Key: `${tabloId}/${filePath}`,
            })
          );

          const uploadedBy =
            headResponse.Metadata?.["uploaded-by"] ?? headResponse.Metadata?.uploaded_by ?? null;

          if (uploadedBy !== user.id) {
            return c.json({ error: "You can only delete files you uploaded" }, 403);
          }
        } catch (error) {
          const statusCode = (error as { $metadata?: { httpStatusCode?: number } })?.$metadata
            ?.httpStatusCode;
          if (statusCode === 404) {
            return c.json({ error: "You can only delete files you uploaded" }, 403);
          }

          console.error("Error checking file ownership:", error);
          return c.json({ error: "Failed to verify file ownership" }, 500);
        }
      }

      await s3_client.send(
        new DeleteObjectCommand({
          Bucket: "tablo-data",
          Key: `${tabloId}/${filePath}`,
        })
      );
      fileNamesCache.delete(tabloId);

      return c.json({
        message: "File deleted successfully",
        fileName: filePath,
        tabloId,
      });
    } catch (error) {
      console.error("Error deleting file:", error);
      return c.json({ error: "Failed to delete file" }, 500);
    }
  });

// ============================================
// FOLDER ENDPOINTS
// ============================================

// Helper to get or create folder metadata
const getFolderMetadata = async (
  s3_client: S3Client,
  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 {
    // File doesn't exist, return default
  }
  const emptyMetadata = { folders: [], version: 1 };
  setCacheValue(foldersCache, tabloId, emptyMetadata);
  return emptyMetadata;
};

// Helper to save folder metadata
const saveFolderMetadata = async (
  s3_client: S3Client,
  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);
};

// GET /tablo-data/:tabloId/folders - Get all folders for a tablo
const getTabloFolders = factory.createHandlers(checkTabloMember, 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(metadata);
  } catch (error) {
    console.error("Error fetching folders:", error);
    return c.json({ error: "Failed to fetch folders" }, 500);
  }
});

// POST /tablo-data/:tabloId/folders - Create a new folder (admin only)
const createTabloFolder = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, checkTabloAdmin, async (c) => {
    const tabloId = c.req.param("tabloId");
    const s3_client = c.get("s3_client");
    const user = c.get("user");

    try {
      const body = await c.req.json();
      const { name, description } = body;

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

      const metadata = await getFolderMetadata(s3_client, tabloId);

      // Check for duplicate folder names
      if (metadata.folders.some((f) => f.name.toLowerCase() === name.trim().toLowerCase())) {
        return c.json({ error: "A folder with this name already exists" }, 400);
      }

      const newFolder: TabloFolder = {
        id: generateFolderId(),
        name: name.trim(),
        description: description?.trim() || undefined,
        createdAt: new Date().toISOString(),
        createdBy: user.id,
      };

      metadata.folders.push(newFolder);
      metadata.version += 1;

      await saveFolderMetadata(s3_client, tabloId, metadata);

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

// PUT /tablo-data/:tabloId/folders/:folderId - Update a folder (admin only)
const updateTabloFolder = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, checkTabloAdmin, 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, description } = body;

      const metadata = await getFolderMetadata(s3_client, tabloId);
      const folderIndex = metadata.folders.findIndex((f) => f.id === folderId);

      if (folderIndex === -1) {
        return c.json({ error: "Folder not found" }, 404);
      }

      // Check for duplicate folder names (excluding current folder)
      if (
        name &&
        metadata.folders.some(
          (f, idx) => idx !== folderIndex && f.name.toLowerCase() === name.trim().toLowerCase()
        )
      ) {
        return c.json({ error: "A folder with this name already exists" }, 400);
      }

      // Update folder
      if (name) {
        metadata.folders[folderIndex].name = name.trim();
      }
      if (description !== undefined) {
        metadata.folders[folderIndex].description = description?.trim() || undefined;
      }
      metadata.version += 1;

      await saveFolderMetadata(s3_client, tabloId, metadata);

      return c.json({
        message: "Folder updated successfully",
        folder: metadata.folders[folderIndex],
      });
    } catch (error) {
      console.error("Error updating folder:", error);
      return c.json({ error: "Failed to update folder" }, 500);
    }
  });

// DELETE /tablo-data/:tabloId/folders/:folderId - Delete a folder (admin only)
const deleteTabloFolder = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  factory.createHandlers(middlewareManager.regularUserCheck, checkTabloAdmin, 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 folderIndex = metadata.folders.findIndex((f) => f.id === folderId);

      if (folderIndex === -1) {
        return c.json({ error: "Folder not found" }, 404);
      }

      const deletedFolder = metadata.folders[folderIndex];
      metadata.folders.splice(folderIndex, 1);
      metadata.version += 1;

      await saveFolderMetadata(s3_client, tabloId, metadata);

      // Note: Files in the folder are NOT deleted, they become "orphaned" / unorganized
      return c.json({
        message: "Folder deleted successfully",
        folder: deletedFolder,
      });
    } catch (error) {
      console.error("Error deleting folder:", error);
      return c.json({ error: "Failed to delete folder" }, 500);
    }
  });

// ============================================
// ROUTER SETUP
// ============================================

export const getTabloDataRouter = () => {
  const tabloDataRouter = new Hono();
  const middlewareManager = MiddlewareManager.getInstance();

  tabloDataRouter.use(middlewareManager.auth);
  tabloDataRouter.use(middlewareManager.r2);

  // All-tablos file listing (must be before /:tabloId routes)
  tabloDataRouter.get("/all-filenames", ...getAllTablosFilenames);

  // File endpoints
  tabloDataRouter.get("/:tabloId/filenames", ...getTabloFilenames);

  // Folder endpoints (must be defined before wildcard file routes)
  tabloDataRouter.get("/:tabloId/folders", ...getTabloFolders);
  tabloDataRouter.post("/:tabloId/folders", ...createTabloFolder(middlewareManager));
  tabloDataRouter.put("/:tabloId/folders/:folderId", ...updateTabloFolder(middlewareManager));
  tabloDataRouter.delete("/:tabloId/folders/:folderId", ...deleteTabloFolder(middlewareManager));

  // File routes using wildcard to support nested paths (e.g., "folder-123/file.pdf")
  // These must be defined after the specific routes above
  tabloDataRouter.get("/:tabloId/file/:path{.+}", ...getTabloFile);
  tabloDataRouter.post("/:tabloId/file/:path{.+}", ...postTabloFile);
  tabloDataRouter.delete("/:tabloId/file/:path{.+}", ...deleteTabloFile(middlewareManager));

  // Legacy routes for backward compatibility (single-level file names only)
  tabloDataRouter.get("/:tabloId/:fileName", ...getTabloFile);
  tabloDataRouter.post("/:tabloId/:fileName", ...postTabloFile);
  tabloDataRouter.delete("/:tabloId/:fileName", ...deleteTabloFile(middlewareManager));

  return tabloDataRouter;
};
