/**
 * quota.ts — Storage and member-limit enforcement for Tia's free tier.
 *
 * Architecture:
 *   Pure functions (no DB I/O) → fully unit-testable without a real database.
 *   DB-bound functions         → thin wrappers that call the pure functions
 *                                with data fetched from Postgres.
 *
 * Payment abstraction:
 *   All enforcement reads family plan state through isPaidFamily().
 *   To upgrade a family, flip families.tier to any non-'free' value.
 *   The actual Razorpay (or other) integration only needs to call that one field.
 */

import { sql } from "@/db";
import { formatBytes } from "@/lib/format-bytes";
export { formatBytes } from "@/lib/format-bytes";

// ─── Constants ───────────────────────────────────────────────────────────────

export const FREE_STORAGE_LIMIT_BYTES = 1_073_741_824; // 1 GiB
export const FREE_MEMBER_LIMIT = 2;

// Storage threshold at which we show a "approaching limit" nudge (80 %).
export const STORAGE_WARN_THRESHOLD = 0.8;

// ─── Pure helpers (unit-testable, no DB) ─────────────────────────────────────

/**
 * Returns true if the family is on a paid plan.
 * Any tier value other than 'free' (or null/undefined) is treated as paid.
 * This is the single gate; swap payment providers by only touching this function.
 */
export function isPaidFamily(
  tier: string | null | undefined
): boolean {
  if (!tier) return false;
  return tier !== "free";
}

/**
 * Returns true if adding declaredBytes would push usage over the limit.
 */
export function wouldExceedQuota(
  currentUsageBytes: number,
  declaredBytes: number,
  limitBytes: number
): boolean {
  return currentUsageBytes + declaredBytes > limitBytes;
}

/**
 * Returns true if the member count is already at or over the free-tier limit.
 * Paid families: always false (no enforcement).
 */
export function isAtMemberLimit(
  currentCount: number,
  maxMembers: number,
  paid: boolean
): boolean {
  if (paid) return false;
  return currentCount >= maxMembers;
}

// ─── Result types ─────────────────────────────────────────────────────────────

export type StorageQuotaResult =
  | { allowed: true;  usedBytes: number; limitBytes: number }
  | {
      allowed: false;
      reason: "storage_quota_exceeded";
      usedBytes: number;
      limitBytes: number;
      message: string;
    };

export type MemberLimitResult =
  | { allowed: true;  currentCount: number; limit: number }
  | {
      allowed: false;
      reason: "member_limit_reached";
      currentCount: number;
      limit: number;
      message: string;
    };

export interface StorageUsageInfo {
  usedBytes: number;
  limitBytes: number;
  /** 0–1 fraction. >1 means over quota (possible after downgrade). */
  fraction: number;
  approaching: boolean; // fraction >= STORAGE_WARN_THRESHOLD
  exceeded: boolean;    // fraction >= 1
  isPaid: boolean;
}

// ─── DB-bound queries ─────────────────────────────────────────────────────────

/**
 * Derived storage usage: SUM(size_bytes) across memories + attachments.
 * Excludes memories still in 'uploading' status (file not yet in R2).
 * Uses CAST to bigint to avoid integer overflow at the SQL level.
 */
export async function getFamilyStorageUsage(familyId: string): Promise<number> {
  const [mRow, aRow] = await Promise.all([
    sql<{ bytes: string }[]>`
      SELECT COALESCE(SUM(size_bytes::bigint), 0) AS bytes
      FROM memories
      WHERE family_id = ${familyId}
        AND processing_status != 'uploading'
        AND size_bytes IS NOT NULL
    `,
    sql<{ bytes: string }[]>`
      SELECT COALESCE(SUM(size_bytes::bigint), 0) AS bytes
      FROM attachments
      WHERE family_id = ${familyId}
        AND size_bytes IS NOT NULL
    `,
  ]);
  return Number(mRow[0]?.bytes ?? 0) + Number(aRow[0]?.bytes ?? 0);
}

/**
 * Returns full usage info for a family (for the storage meter UI).
 */
export async function getStorageInfo(familyId: string): Promise<StorageUsageInfo> {
  const [familyRow] = await sql<{ tier: string | null }[]>`
    SELECT tier FROM families WHERE id = ${familyId} LIMIT 1
  `;
  const paid = isPaidFamily(familyRow?.tier);
  const usedBytes = await getFamilyStorageUsage(familyId);
  const limitBytes = paid ? Infinity : FREE_STORAGE_LIMIT_BYTES;
  const fraction = paid ? 0 : usedBytes / FREE_STORAGE_LIMIT_BYTES;

  return {
    usedBytes,
    limitBytes,
    fraction,
    approaching: !paid && fraction >= STORAGE_WARN_THRESHOLD && fraction < 1,
    exceeded: !paid && fraction >= 1,
    isPaid: paid,
  };
}

/**
 * Storage quota gate — called at presigned-URL issuance.
 * declaredBytes is the client-reported file size (untrusted, but used for the gate).
 * On-confirm, actual size is reconciled; see reconcileActualSize().
 */
export async function checkStorageQuota(
  familyId: string,
  declaredBytes: number
): Promise<StorageQuotaResult> {
  const [familyRow] = await sql<{ tier: string | null }[]>`
    SELECT tier FROM families WHERE id = ${familyId} LIMIT 1
  `;

  if (isPaidFamily(familyRow?.tier)) {
    const usedBytes = await getFamilyStorageUsage(familyId);
    return { allowed: true, usedBytes, limitBytes: Infinity };
  }

  const limitBytes = FREE_STORAGE_LIMIT_BYTES;
  const usedBytes = await getFamilyStorageUsage(familyId);

  if (wouldExceedQuota(usedBytes, declaredBytes, limitBytes)) {
    return {
      allowed: false,
      reason: "storage_quota_exceeded",
      usedBytes,
      limitBytes,
      message: `Storage quota exceeded. You have used ${formatBytes(usedBytes)} of your ${formatBytes(limitBytes)} limit. Delete some memories or upgrade to continue uploading.`,
    };
  }

  return { allowed: true, usedBytes, limitBytes };
}

/**
 * After a confirmed R2 upload, checks if the actual object size would push
 * the family over quota. Returns whether the upload should be kept.
 */
export async function reconcileActualSize(
  familyId: string,
  memoryId: string,
  actualBytes: number
): Promise<{ keep: boolean; reason?: string }> {
  const [familyRow] = await sql<{ tier: string | null }[]>`
    SELECT tier FROM families WHERE id = ${familyId} LIMIT 1
  `;

  // Always update the stored size to the actual value regardless of plan
  await sql`
    UPDATE memories SET size_bytes = ${actualBytes} WHERE id = ${memoryId} AND family_id = ${familyId}
  `;

  if (isPaidFamily(familyRow?.tier)) return { keep: true };

  // Re-derive usage with the updated size
  const usedBytes = await getFamilyStorageUsage(familyId);
  if (usedBytes > FREE_STORAGE_LIMIT_BYTES) {
    return {
      keep: false,
      reason: `Actual file size (${formatBytes(actualBytes)}) pushed family over the ${formatBytes(FREE_STORAGE_LIMIT_BYTES)} quota.`,
    };
  }

  return { keep: true };
}

// ─── Child limit ─────────────────────────────────────────────────────────────

export type ChildLimitResult =
  | { allowed: true;  currentCount: number; limit: number }
  | {
      allowed: false;
      reason: "child_limit_reached";
      currentCount: number;
      limit: number;
      message: string;
    };

/**
 * Child limit gate — called at child creation.
 * Free families are limited to max_children (default 1).
 * Paid families: always allowed.
 */
export async function checkChildLimit(familyId: string): Promise<ChildLimitResult> {
  const [row] = await sql<{ count: string; max_children: number | null; tier: string | null }[]>`
    SELECT
      COUNT(c.id)::text AS count,
      f.max_children,
      f.tier
    FROM families f
    LEFT JOIN children c ON c.family_id = f.id
    WHERE f.id = ${familyId}
    GROUP BY f.id
  `;

  if (!row) return { allowed: false, reason: "child_limit_reached", currentCount: 0, limit: 1, message: "Family not found." };

  const currentCount = Number(row.count);
  const limit = row.max_children ?? 1;

  if (isPaidFamily(row.tier)) return { allowed: true, currentCount, limit };

  if (currentCount >= limit) {
    return {
      allowed: false,
      reason: "child_limit_reached",
      currentCount,
      limit,
      message: `Your free plan supports ${limit} baby profile. Upgrade to track multiple children.`,
    };
  }

  return { allowed: true, currentCount, limit };
}

/**
 * Member limit gate — called at invite creation (not acceptance).
 * COUNT(family_members) is the source of truth; no separate counter.
 */
export async function checkMemberLimit(familyId: string): Promise<MemberLimitResult> {
  const [row] = await sql<{ count: string; max_members: number | null; tier: string | null }[]>`
    SELECT
      COUNT(fm.id)::text AS count,
      f.max_members,
      f.tier
    FROM families f
    LEFT JOIN family_members fm ON fm.family_id = f.id
    WHERE f.id = ${familyId}
    GROUP BY f.id
  `;

  if (!row) return { allowed: false, reason: "member_limit_reached", currentCount: 0, limit: FREE_MEMBER_LIMIT, message: "Family not found." };

  const currentCount = Number(row.count);
  const limit = row.max_members ?? FREE_MEMBER_LIMIT;

  if (isAtMemberLimit(currentCount, limit, isPaidFamily(row.tier))) {
    return {
      allowed: false,
      reason: "member_limit_reached",
      currentCount,
      limit,
      message: `Your family has reached its member limit (${currentCount}/${limit}). Upgrade to invite more caregivers.`,
    };
  }

  return { allowed: true, currentCount, limit };
}
