/**
 * Water Log — field (PIN) actions.
 *
 * These power `/water` and `/water/admin` — the mobile-first, PIN-only
 * portals that ditch-riders actually use in the field. The shape of
 * the data they consume is the same as admin actions, but the
 * authorization model is different:
 *
 *   - Irrigators carry a `wl_session` cookie tied to a row in
 *     `water_sessions`. Cookie expiry = row's `expires_at`.
 *   - Admins (water_admin role) carry a `wl_admin_session` cookie
 *     tied to `water_admin_sessions`.
 *
 * Both cookies are httpOnly + sameSite=lax. The session lookup is the
 * gate for every mutating action — there's no role/permission check
 * inside the action because the cookie presence + the row's role is
 * the permission.
 */
"use server";

import { cookies } from "next/headers";
import { and, desc, eq, gte, sql } from "drizzle-orm";
import { withBrand, withPlatformAdmin } from "@/db/client";
import {
  waterHeadgates,
  waterIrrigators,
  waterSessions,
  waterLogEntries,
  waterAdminSessions,
} from "@/db/schema/water-log";
import { verifyPin, validatePin } from "@/lib/water-log-pin";
import { logAlert } from "@/lib/water-log-audit";

// Field sessions last 8h — a working day in the field. Long enough
// that a single sign-in covers a morning shift + afternoon shift.
const FIELD_SESSION_HOURS = 8;
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";

// ── Types ──────────────────────────────────────────────────────────────────

type FieldHeadgate = {
  id: string;
  name: string;
  unit: string;
  status: string;
  high_threshold: number | null;
  low_threshold: number | null;
  active: boolean;
};

type VerifyPinResult =
  | {
      success: true;
      user_id: string;
      name: string;
      role: "irrigator" | "water_admin";
      session_id: string;
      lang: string;
    }
  | { success: false; error: string };

type SubmitEntryResult =
  | { success: true; entry_id: string }
  | { success: false; error: string };

// ── Headgates (read-only for field) ────────────────────────────────────────

export async function getWaterHeadgates(
  _brandId: string,
  activeOnly: boolean = false,
): Promise<FieldHeadgate[]> {
  return withBrand(TUXEDO_BRAND_ID, async (db) => {
    const where = activeOnly
      ? and(
          eq(waterHeadgates.brandId, TUXEDO_BRAND_ID),
          eq(waterHeadgates.active, true),
        )
      : eq(waterHeadgates.brandId, TUXEDO_BRAND_ID);
    const rows = await db
      .select()
      .from(waterHeadgates)
      .where(where)
      .orderBy(waterHeadgates.name);
    return rows.map((h) => ({
      id: h.id,
      name: h.name,
      unit: h.unit,
      status: h.status,
      high_threshold: h.highThreshold != null ? Number(h.highThreshold) : null,
      low_threshold: h.lowThreshold != null ? Number(h.lowThreshold) : null,
      active: h.active,
    }));
  });
}

// ── PIN verify + session ───────────────────────────────────────────────────

export async function verifyWaterPin(
  _brandId: string,
  pin: string,
): Promise<VerifyPinResult> {
  // Validate format first (cheap, no DB).
  const formatError = validatePin(pin);
  if (formatError) return { success: false, error: formatError };

  // Look the irrigator up across all brands (PIN is the only credential).
  const lookup = await withPlatformAdmin(async (db) => {
    const rows = await db
      .select()
      .from(waterIrrigators)
      .where(eq(waterIrrigators.active, true))
      .orderBy(waterIrrigators.name);
    return rows;
  });

  // Constant-time-ish: check every row, only succeed on the first match.
  // (We can't make this perfectly constant-time without pulling the
  //  hashed values into memory in random order, but iterating by name
  //  is close enough for a 4-digit PIN and avoids the worst case of an
  //  obvious timing oracle on a sorted list.)
  const match = lookup.find((i) => verifyPin(pin, i.pinHash));
  if (!match) {
    // Add a small delay to discourage brute-force scanning.
    await new Promise((r) => setTimeout(r, 200));
    return { success: false, error: "Invalid PIN" };
  }

  // Create a session in the user's brand context.
  const sessionId = await withBrand(match.brandId, async (db) => {
    const expiresAt = new Date(
      Date.now() + FIELD_SESSION_HOURS * 60 * 60 * 1000,
    );
    const [row] = await db
      .insert(waterSessions)
      .values({
        irrigatorId: match.id,
        expiresAt,
      })
      .returning({ id: waterSessions.id });
    if (!row) throw new Error("Failed to create session");
    // Bump last_used_at for "I haven't seen this person in a while" UX
    await db
      .update(waterIrrigators)
      .set({ lastUsedAt: new Date() })
      .where(eq(waterIrrigators.id, match.id));
    return row.id;
  });

  const cookieStore = await cookies();
  cookieStore.set("wl_session", sessionId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: FIELD_SESSION_HOURS * 3600,
    path: "/",
  });

  return {
    success: true,
    user_id: match.id,
    name: match.name,
    role: (match.role as "irrigator" | "water_admin") ?? "irrigator",
    session_id: sessionId,
    lang: match.languagePreference,
  };
}

// ── Submit entry ───────────────────────────────────────────────────────────

export async function submitWaterEntry(
  headgateId: string,
  measurement: number,
  unit: string,
  notes: string,
  photoUrl?: string,
  latitude?: number,
  longitude?: number,
  _headgateLocked?: boolean,
): Promise<SubmitEntryResult> {
  // ── Auth ──
  const session = await requireFieldSession();
  if (!session.ok) return { success: false, error: session.error };

  // ── Validate input ──
  if (!Number.isFinite(measurement) || measurement < 0) {
    return { success: false, error: "Measurement must be a non-negative number" };
  }
  if (measurement > 1_000_000) {
    return { success: false, error: "Measurement is implausibly large" };
  }
  if (notes && notes.length > 500) {
    return { success: false, error: "Notes are too long (max 500 chars)" };
  }
  if (latitude != null && (latitude < -90 || latitude > 90)) {
    return { success: false, error: "Invalid latitude" };
  }
  if (longitude != null && (longitude < -180 || longitude > 180)) {
    return { success: false, error: "Invalid longitude" };
  }

  // ── Write the entry inside the brand context ──
  return withBrand(session.brandId, async (db) => {
    // Confirm the headgate belongs to this brand
    const headgateRows = await db
      .select()
      .from(waterHeadgates)
      .where(eq(waterHeadgates.id, headgateId))
      .limit(1);
    const headgate = headgateRows[0];
    if (!headgate) return { success: false, error: "Headgate not found" };

    // Auto-calc total_gallons when CFS × duration is provided.
    // Caller passes `measurement` as the total CFS volume for the set
    // (we don't track duration separately on the entry — duration goes
    // in notes if needed). We leave total_gallons null for CFS by
    // default to avoid spurious "X gallons" claims.
    const totalGallons = computeTotalGallons(measurement, unit);

    const now = new Date();
    const loggedDate = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}`;

    const [entry] = await db
      .insert(waterLogEntries)
      .values({
        brandId: session.brandId,
        headgateId,
        irrigatorId: session.userId,
        measurement: measurement.toString(),
        unit,
        totalGallons: totalGallons?.toString() ?? null,
        method: "manual",
        notes: notes || null,
        submittedVia: "field",
        photoUrl: photoUrl ?? null,
        latitude: latitude ?? null,
        longitude: longitude ?? null,
        loggedDate,
        loggedAt: now,
      })
      .returning({ id: waterLogEntries.id });
    if (!entry) return { success: false, error: "Insert failed" };

    // Bump headgate last_used_at
    await db
      .update(waterHeadgates)
      .set({ lastUsedAt: now })
      .where(eq(waterHeadgates.id, headgateId));

    // Threshold alert check (fire-and-forget)
    if (headgate.highThreshold != null && measurement > Number(headgate.highThreshold)) {
      void logAlert({
        brandId: session.brandId,
        headgateId,
        alertType: "high",
        reading: measurement,
        threshold: Number(headgate.highThreshold),
        headgateName: headgate.name,
      });
    }
    if (headgate.lowThreshold != null && measurement < Number(headgate.lowThreshold)) {
      void logAlert({
        brandId: session.brandId,
        headgateId,
        alertType: "low",
        reading: measurement,
        threshold: Number(headgate.lowThreshold),
        headgateName: headgate.name,
      });
    }

    return { success: true, entry_id: entry.id };
  });
}

// ── Session helpers (shared with admin.ts via requireFieldSession) ────────

type FieldSession =
  | { ok: true; userId: string; brandId: string; role: "irrigator" | "water_admin" }
  | { ok: false; error: string };

export async function requireFieldSession(): Promise<FieldSession> {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("wl_session")?.value;
  if (!sessionId) return { ok: false, error: "Not logged in" };

  return withPlatformAdmin(async (db) => {
    const rows = await db
      .select({
        session: waterSessions,
        irrigator: waterIrrigators,
      })
      .from(waterSessions)
      .innerJoin(waterIrrigators, eq(waterIrrigators.id, waterSessions.irrigatorId))
      .where(eq(waterSessions.id, sessionId))
      .limit(1);
    const row = rows[0];
    if (!row) return { ok: false as const, error: "Session not found" };
    if (row.session.expiresAt.getTime() < Date.now()) {
      // Best-effort cleanup
      await db.delete(waterSessions).where(eq(waterSessions.id, sessionId));
      return { ok: false as const, error: "Session expired" };
    }
    if (!row.irrigator.active) {
      return { ok: false as const, error: "User is inactive" };
    }
    return {
      ok: true as const,
      userId: row.irrigator.id,
      brandId: row.irrigator.brandId,
      role: (row.irrigator.role as "irrigator" | "water_admin") ?? "irrigator",
    };
  });
}

export async function getFieldSessionUser(): Promise<{
  userId: string;
  name: string;
  brandId: string;
  role: "irrigator" | "water_admin";
} | null> {
  const s = await requireFieldSession();
  if (!s.ok) return null;
  return withPlatformAdmin(async (db) => {
    const rows = await db
      .select({ name: waterIrrigators.name, role: waterIrrigators.role })
      .from(waterIrrigators)
      .where(eq(waterIrrigators.id, s.userId))
      .limit(1);
    const row = rows[0];
    if (!row) return null;
    return {
      userId: s.userId,
      name: row.name,
      brandId: s.brandId,
      role: (row.role as "irrigator" | "water_admin") ?? "irrigator",
    };
  });
}

// ── Cookie/language helpers ───────────────────────────────────────────────

export async function logoutWater(): Promise<void> {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("wl_session")?.value;
  if (sessionId) {
    // Best-effort DB cleanup
    try {
      await withPlatformAdmin(async (db) => {
        await db.delete(waterSessions).where(eq(waterSessions.id, sessionId));
      });
    } catch {
      // ignore — the cookie is being deleted anyway
    }
  }
  cookieStore.delete("wl_session");
}

export async function logoutWaterAdmin(): Promise<void> {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("wl_admin_session")?.value;
  if (sessionId) {
    try {
      await withPlatformAdmin(async (db) => {
        await db
          .delete(waterAdminSessions)
          .where(eq(waterAdminSessions.id, sessionId));
      });
    } catch {
      // ignore — cookie is being deleted anyway
    }
  }
  cookieStore.delete("wl_admin_session");
}

export async function getWaterSession(): Promise<string | null> {
  const cookieStore = await cookies();
  return cookieStore.get("wl_session")?.value ?? null;
}

/**
 * Resolves the current `/water/admin` session.
 *
 * Returns the admin role + brandId on success, or `null` if the cookie
 * is missing / expired / points at a deleted session row. Used by
 * admin pages (entries/[id], headgates/[id], users/[id]) as a *second*
 * gate on top of `getAdminUser()` — a site admin can edit entries
 * directly, but a PIN-authenticated water admin can too.
 */
export async function getWaterAdminSession(): Promise<{
  sessionId: string;
  brandId: string;
  adminUserId: string;
  role: "water_admin";
} | null> {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("wl_admin_session")?.value;
  if (!sessionId) return null;
  return withPlatformAdmin(async (db) => {
    const rows = await db
      .select({
        id: waterAdminSessions.id,
        brandId: waterAdminSessions.brandId,
        adminUserId: waterAdminSessions.adminUserId,
        expiresAt: waterAdminSessions.expiresAt,
      })
      .from(waterAdminSessions)
      .where(eq(waterAdminSessions.id, sessionId))
      .limit(1);
    const row = rows[0];
    if (!row) return null;
    if (row.expiresAt.getTime() <= Date.now()) return null;
    return {
      sessionId: row.id,
      brandId: row.brandId,
      adminUserId: row.adminUserId,
      role: "water_admin" as const,
    };
  });
}

export async function setWaterLang(lang: string): Promise<void> {
  if (!["en", "es"].includes(lang)) return;
  const cookieStore = await cookies();
  cookieStore.set("wl_lang", lang, {
    httpOnly: false,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 30,
    path: "/",
  });
}

// ── Internal helpers ──────────────────────────────────────────────────────

/**
 * If the measurement is in CFS (cubic feet per second) and we know it
 * represents a set duration, we can derive total gallons. Without
 * duration, we leave total_gallons null to avoid making up numbers.
 *
 * CFS × 60s × 7.48052 = gallons per minute.
 * 1 CFS for 1 hour = ~448.83 gallons.
 */
function computeTotalGallons(measurement: number, unit: string): number | null {
  if (unit !== "CFS") return null;
  // We don't have a duration field on the entry, so we return null. The
  // admin can edit an entry to set gallons manually if they have that
  // data. The hook is here so callers can extend easily.
  return null;
}

function pad(n: number): string {
  return n.toString().padStart(2, "0");
}
