import { Hono } from "hono";
import { desc, eq, sql } from "drizzle-orm";
import dayjs from "dayjs";
import { useDrizzle } from "@/lib/db.js";
import { chargePoint, connector, meterValue } from "@/db/schema.js";
import type { SampledValue } from "@/db/schema.js";
import { ocppConnections } from "@/ocpp/handler.js";
import { generateOcppPassword, hashOcppPassword } from "@/lib/ocpp-auth.js";
import type { HonoEnv } from "@/types/hono.ts";

const app = new Hono<HonoEnv>();

/** GET /api/charge-points  —  list all charge points with connectors */
app.get("/", async (c) => {
  const db = useDrizzle();
  const isAdmin = c.get("user")?.role === "admin";

  const cps = await db
    .select()
    .from(chargePoint)
    .where(isAdmin ? undefined : eq(chargePoint.registrationStatus, "Accepted"))
    .orderBy(desc(chargePoint.createdAt));

  if (!cps.length) return c.json([]);

  const cpIdList = sql.raw(`array[${cps.map((cp) => `'${cp.id}'`).join(",")}]`);

  // connectorId > 0: real connectors to display
  const connectors = await db
    .select()
    .from(connector)
    .where(sql`${connector.chargePointId} = any(${cpIdList}) and ${connector.connectorId} > 0`);

  // connectorId = 0: whole-station status row
  const cpStatusRows = await db
    .select({
      chargePointId: connector.chargePointId,
      status: connector.status,
      errorCode: connector.errorCode,
    })
    .from(connector)
    .where(sql`${connector.chargePointId} = any(${cpIdList}) and ${connector.connectorId} = 0`);

  const connectorsByCP: Record<string, typeof connectors> = {};
  for (const conn of connectors) {
    if (!connectorsByCP[conn.chargePointId]) connectorsByCP[conn.chargePointId] = [];
    connectorsByCP[conn.chargePointId].push(conn);
  }

  const cpStatusByCP: Record<string, { status: string; errorCode: string }> = {};
  for (const row of cpStatusRows) {
    cpStatusByCP[row.chargePointId] = { status: row.status, errorCode: row.errorCode };
  }

  return c.json(
    cps.map((cp) => ({
      ...cp,
      connectors: connectorsByCP[cp.id] ?? [],
      chargePointStatus: cpStatusByCP[cp.id]?.status ?? null,
      chargePointErrorCode: cpStatusByCP[cp.id]?.errorCode ?? null,
    })),
  );
});

/** POST /api/charge-points  —  manually pre-register a charge point */
app.post("/", async (c) => {
  if (c.get("user")?.role !== "admin") return c.json({ error: "Forbidden" }, 403);
  const db = useDrizzle();
  const body = await c.req.json<{
    chargePointIdentifier: string;
    chargePointVendor?: string;
    chargePointModel?: string;
    registrationStatus?: "Accepted" | "Pending" | "Rejected";
    feePerKwh?: number;
    pricingMode?: "fixed" | "tou";
    deviceName?: string;
  }>();

  if (!body.chargePointIdentifier?.trim()) {
    return c.json({ error: "chargePointIdentifier is required" }, 400);
  }
  if (body.feePerKwh !== undefined && (!Number.isInteger(body.feePerKwh) || body.feePerKwh < 0)) {
    return c.json({ error: "feePerKwh must be a non-negative integer" }, 400);
  }
  if (body.pricingMode !== undefined && !["fixed", "tou"].includes(body.pricingMode)) {
    return c.json({ error: "pricingMode must be 'fixed' or 'tou'" }, 400);
  }

  const plainPassword = generateOcppPassword();
  const passwordHash = await hashOcppPassword(plainPassword);

  const [created] = await db
    .insert(chargePoint)
    .values({
      id: crypto.randomUUID(),
      chargePointIdentifier: body.chargePointIdentifier.trim(),
      chargePointVendor: body.chargePointVendor?.trim() || "Unknown",
      chargePointModel: body.chargePointModel?.trim() || "Unknown",
      registrationStatus: body.registrationStatus ?? "Pending",
      feePerKwh: body.feePerKwh ?? 0,
      pricingMode: body.pricingMode ?? "fixed",
      deviceName: body.deviceName?.trim() || null,
      passwordHash,
    })
    .returning()
    .catch((err: Error) => {
      if (err.message.includes("unique")) throw Object.assign(err, { status: 409 });
      throw err;
    });

  // 明文密码仅在创建时返回一次，之后不可再查
  return c.json({ ...created, passwordHash: undefined, plainPassword, connectors: [] }, 201);
});

/** GET /api/charge-points/connections  —  list currently active OCPP WebSocket connections */
app.get("/connections", (c) => {
  return c.json({
    connectedIdentifiers: Array.from(ocppConnections.keys()),
  });
});

/** GET /api/charge-points/:id  —  single charge point */
app.get("/:id", async (c) => {
  const db = useDrizzle();
  const id = c.req.param("id");
  const isAdmin = c.get("user")?.role === "admin";

  const [cp] = await db.select().from(chargePoint).where(eq(chargePoint.id, id)).limit(1);

  if (!cp) return c.json({ error: "Not found" }, 404);

  const allConnectors = await db.select().from(connector).where(eq(connector.chargePointId, id));
  const cpStatus = allConnectors.find((conn) => conn.connectorId === 0);
  const displayConnectors = allConnectors.filter((conn) => conn.connectorId > 0);
  const [latestMeter] = await db
    .select({
      timestamp: meterValue.timestamp,
      sampledValues: meterValue.sampledValues,
    })
    .from(meterValue)
    .where(eq(meterValue.chargePointId, id))
    .orderBy(desc(meterValue.timestamp), desc(meterValue.receivedAt))
    .limit(1);

  const meterHistory = isAdmin
    ? (
        await db
          .select({
            connectorNumber: meterValue.connectorNumber,
            timestamp: meterValue.timestamp,
            sampledValues: meterValue.sampledValues,
          })
          .from(meterValue)
          .where(eq(meterValue.chargePointId, id))
          .orderBy(desc(meterValue.timestamp), desc(meterValue.receivedAt))
          .limit(24)
      ).reverse()
    : [];

  return c.json({
    ...cp,
    connectors: displayConnectors,
    chargePointStatus: cpStatus?.status ?? null,
    chargePointErrorCode: cpStatus?.errorCode ?? null,
    latestMeterTimestamp: latestMeter?.timestamp ?? null,
    latestMeterValues: ((latestMeter?.sampledValues as SampledValue[] | undefined) ?? []),
    meterHistory,
  });
});

/** PATCH /api/charge-points/:id  —  update charge point fields */
app.patch("/:id", async (c) => {
  if (c.get("user")?.role !== "admin") return c.json({ error: "Forbidden" }, 403);
  const db = useDrizzle();
  const id = c.req.param("id");
  const body = await c.req.json<{
    feePerKwh?: number;
    pricingMode?: "fixed" | "tou";
    registrationStatus?: string;
    chargePointVendor?: string;
    chargePointModel?: string;
    deviceName?: string | null;
  }>();

  const set: {
    feePerKwh?: number;
    pricingMode?: "fixed" | "tou";
    registrationStatus?: "Accepted" | "Pending" | "Rejected";
    chargePointVendor?: string;
    chargePointModel?: string;
    deviceName?: string | null;
    updatedAt: Date;
  } = { updatedAt: dayjs().toDate() };

  if (body.feePerKwh !== undefined) {
    if (!Number.isInteger(body.feePerKwh) || body.feePerKwh < 0) {
      return c.json({ error: "feePerKwh must be a non-negative integer (unit: fen/kWh)" }, 400);
    }
    set.feePerKwh = body.feePerKwh;
  }
  if (body.registrationStatus !== undefined) {
    if (!["Accepted", "Pending", "Rejected"].includes(body.registrationStatus)) {
      return c.json({ error: "invalid registrationStatus" }, 400);
    }
    set.registrationStatus = body.registrationStatus as "Accepted" | "Pending" | "Rejected";
  }
  if (body.chargePointVendor !== undefined)
    set.chargePointVendor = body.chargePointVendor.trim() || "Unknown";
  if (body.chargePointModel !== undefined)
    set.chargePointModel = body.chargePointModel.trim() || "Unknown";
  if ("deviceName" in body) set.deviceName = body.deviceName?.trim() || null;
  if (body.pricingMode !== undefined) {
    if (!["fixed", "tou"].includes(body.pricingMode)) {
      return c.json({ error: "pricingMode must be 'fixed' or 'tou'" }, 400);
    }
    set.pricingMode = body.pricingMode;
  }

  const [updated] = await db.update(chargePoint).set(set).where(eq(chargePoint.id, id)).returning();

  if (!updated) return c.json({ error: "Not found" }, 404);

  const allConnectors = await db.select().from(connector).where(eq(connector.chargePointId, id));
  const cpStatus = allConnectors.find((conn) => conn.connectorId === 0);
  const displayConnectors = allConnectors.filter((conn) => conn.connectorId > 0);

  return c.json({
    ...updated,
    connectors: displayConnectors,
    chargePointStatus: cpStatus?.status ?? null,
    chargePointErrorCode: cpStatus?.errorCode ?? null,
  });
});

/** DELETE /api/charge-points/:id  —  delete a charge point (cascades to connectors, transactions, meter values) */
app.delete("/:id", async (c) => {
  if (c.get("user")?.role !== "admin") return c.json({ error: "Forbidden" }, 403);
  const db = useDrizzle();
  const id = c.req.param("id");

  const [deleted] = await db
    .delete(chargePoint)
    .where(eq(chargePoint.id, id))
    .returning({ id: chargePoint.id });

  if (!deleted) return c.json({ error: "Not found" }, 404);

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

/** POST /api/charge-points/:id/reset-password  —  regenerate OCPP Basic Auth password */
app.post("/:id/reset-password", async (c) => {
  if (c.get("user")?.role !== "admin") return c.json({ error: "Forbidden" }, 403);
  const db = useDrizzle();
  const id = c.req.param("id");

  const plainPassword = generateOcppPassword();
  const passwordHash = await hashOcppPassword(plainPassword);

  const [updated] = await db
    .update(chargePoint)
    .set({ passwordHash })
    .where(eq(chargePoint.id, id))
    .returning({ id: chargePoint.id, chargePointIdentifier: chargePoint.chargePointIdentifier });

  if (!updated) return c.json({ error: "Not found" }, 404);

  // 明文密码仅返回一次
  return c.json({ ...updated, plainPassword });
});

export default app;
