import { Hono } from "hono";
import type { Context } from "hono";
import { apiError } from "../lib/errors";
import { logUsage } from "../lib/usage-log";
import {
  blobKey,
  deleteObject,
  headObject,
  presignGet,
  presignPut,
  type BucketLocation,
  type WasabiCreds,
} from "../lib/wasabi";
import type { AppEnv, BucketRow, Env } from "../types";

export const SHA256_RE = /^[a-f0-9]{64}$/;
export const MAX_OBJECT_BYTES = 5_368_709_120;

export function wasabiCreds(env: Env): WasabiCreds {
  return {
    accessKeyId: env.WASABI_ACCESS_KEY_ID,
    secretAccessKey: env.WASABI_SECRET_ACCESS_KEY,
  };
}

export async function getBucket(c: Context<AppEnv>): Promise<BucketLocation> {
  const site = c.get("site");
  const bucket = await c.env.DB.prepare(
    "SELECT id, name, region, site_count FROM buckets WHERE id = ?",
  )
    .bind(site.bucket_id)
    .first<BucketRow>();
  if (!bucket) throw new Error(`bucket ${site.bucket_id} missing for site ${site.id}`);
  return { name: bucket.name, region: bucket.region };
}

export const storage = new Hono<AppEnv>();

storage.post("/blobs/sign-upload", async (c) => {
  const site = c.get("site");
  const body = (await c.req.json().catch(() => null)) as
    | { sha256?: unknown; size_bytes?: unknown }
    | null;
  if (!body || typeof body.sha256 !== "string" || typeof body.size_bytes !== "number") {
    return apiError(c, 400, "invalid_request", "sha256 and size_bytes are required");
  }
  if (!SHA256_RE.test(body.sha256)) {
    return apiError(c, 400, "invalid_sha256", "sha256 must be 64 lowercase hex chars");
  }
  if (
    !Number.isInteger(body.size_bytes) ||
    body.size_bytes <= 0 ||
    body.size_bytes > MAX_OBJECT_BYTES
  ) {
    return apiError(c, 400, "invalid_size", "size_bytes out of accepted range");
  }

  const existing = await c.env.DB.prepare(
    "SELECT status FROM blobs WHERE site_id = ? AND sha256 = ?",
  )
    .bind(site.id, body.sha256)
    .first<{ status: string }>();
  if (existing?.status === "stored") {
    await c.env.DB.prepare(
      "UPDATE blobs SET created_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE site_id = ? AND sha256 = ?",
    )
      .bind(site.id, body.sha256)
      .run();
    return c.json({ status: "exists" });
  }

  const license = await c.env.DB.prepare(
    "SELECT storage_bytes_limit FROM licenses WHERE license_key = ?",
  )
    .bind(site.license_key)
    .first<{ storage_bytes_limit: number }>();
  if (!license) return apiError(c, 403, "invalid_license", "License not found");
  const pending = await c.env.DB.prepare(
    "SELECT COALESCE(SUM(size_bytes), 0) AS n FROM blobs WHERE site_id = ? AND status = 'pending' AND sha256 != ?",
  )
    .bind(site.id, body.sha256)
    .first<{ n: number }>();
  const reserved = site.storage_bytes_used + (pending?.n ?? 0);
  if (reserved + body.size_bytes > license.storage_bytes_limit) {
    return apiError(c, 402, "quota_exceeded", "Storage quota exceeded");
  }

  await c.env.DB.prepare(
    "INSERT INTO blobs (site_id, sha256, size_bytes, status) VALUES (?, ?, ?, 'pending') ON CONFLICT(site_id, sha256) DO UPDATE SET size_bytes = excluded.size_bytes, created_at = strftime('%Y-%m-%dT%H:%M:%SZ','now')",
  )
    .bind(site.id, body.sha256, body.size_bytes)
    .run();
  await logUsage(c.env.DB, site.id, "sign_upload", body.sha256, body.size_bytes);

  const bucket = await getBucket(c);
  const presigned = await presignPut(
    wasabiCreds(c.env),
    bucket,
    blobKey(site.id, body.sha256),
  );
  return c.json({
    status: "upload",
    upload_url: presigned.url,
    expires_at: presigned.expiresAt,
  });
});

storage.post("/blobs/confirm", async (c) => {
  const site = c.get("site");
  const body = (await c.req.json().catch(() => null)) as { sha256?: unknown } | null;
  if (!body || typeof body.sha256 !== "string" || !SHA256_RE.test(body.sha256)) {
    return apiError(c, 400, "invalid_request", "A valid sha256 is required");
  }

  const blob = await c.env.DB.prepare(
    "SELECT status, size_bytes FROM blobs WHERE site_id = ? AND sha256 = ?",
  )
    .bind(site.id, body.sha256)
    .first<{ status: string; size_bytes: number }>();
  if (!blob) return apiError(c, 404, "not_found", "Unknown blob for this site");
  if (blob.status === "stored") return c.json({ status: "stored" });

  const bucket = await getBucket(c);
  const key = blobKey(site.id, body.sha256);
  const realSize = await headObject(wasabiCreds(c.env), bucket, key);

  if (realSize === null) {
    return apiError(c, 400, "not_uploaded", "Object not found in storage");
  }
  if (realSize !== blob.size_bytes) {
    await deleteObject(wasabiCreds(c.env), bucket, key);
    await c.env.DB.prepare("DELETE FROM blobs WHERE site_id = ? AND sha256 = ?")
      .bind(site.id, body.sha256)
      .run();
    return apiError(c, 409, "size_mismatch", "Uploaded size does not match declaration");
  }

  const claimed = await c.env.DB.prepare(
    "UPDATE blobs SET status = 'stored', created_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE site_id = ? AND sha256 = ? AND status <> 'stored'",
  )
    .bind(site.id, body.sha256)
    .run();
  if ((claimed.meta.changes ?? 0) > 0) {
    await c.env.DB.prepare(
      "UPDATE sites SET storage_bytes_used = storage_bytes_used + ? WHERE id = ?",
    )
      .bind(realSize, site.id)
      .run();
    await logUsage(c.env.DB, site.id, "confirm", body.sha256, realSize);
  }
  return c.json({ status: "stored" });
});

storage.post("/blobs/sign-download", async (c) => {
  const site = c.get("site");
  const body = (await c.req.json().catch(() => null)) as { sha256?: unknown } | null;
  if (!body || typeof body.sha256 !== "string" || !SHA256_RE.test(body.sha256)) {
    return apiError(c, 400, "invalid_request", "A valid sha256 is required");
  }

  const blob = await c.env.DB.prepare(
    "SELECT status FROM blobs WHERE site_id = ? AND sha256 = ? AND status = 'stored'",
  )
    .bind(site.id, body.sha256)
    .first<{ status: string }>();
  if (!blob) return apiError(c, 404, "not_found", "Blob not available for this site");

  const bucket = await getBucket(c);
  const presigned = await presignGet(
    wasabiCreds(c.env),
    bucket,
    blobKey(site.id, body.sha256),
  );
  await logUsage(c.env.DB, site.id, "sign_download", body.sha256, null);
  return c.json({ download_url: presigned.url, expires_at: presigned.expiresAt });
});
