import { Hono } from "hono";
import { and, desc, eq, isNull, isNotNull, sql } from "drizzle-orm";
import dayjs from "dayjs";
import { useDrizzle } from "@/lib/db.js";
import { transaction, chargePoint, connector, idTag } from "@/db/schema.js";
import type { SampledValue } from "@/db/schema.js";
import { user } from "@/db/auth-schema.js";
import { sendOcppCall } from "@/ocpp/handler.js";
import { resolveIdTagInfo } from "@/ocpp/actions/authorize.js";
import type { HonoEnv } from "@/types/hono.ts";

const app = new Hono<HonoEnv>();

/**
 * POST /api/transactions/remote-start
 * Send RemoteStartTransaction to a charge point.
 * Non-admin users can only use their own id-tags.
 */
app.post("/remote-start", async (c) => {
  const currentUser = c.get("user");
  if (!currentUser) return c.json({ error: "Unauthorized" }, 401);

  const db = useDrizzle();
  const body = await c.req
    .json<{
      chargePointIdentifier: string;
      connectorId: number;
      idTag: string;
    }>()
    .catch(() => null);

  if (
    !body ||
    !body.chargePointIdentifier?.trim() ||
    !Number.isInteger(body.connectorId) ||
    body.connectorId < 1 ||
    !body.idTag?.trim()
  ) {
    return c.json(
      { error: "chargePointIdentifier, connectorId (>=1), and idTag are required" },
      400,
    );
  }

  // Non-admin: verify idTag belongs to current user
  if (currentUser.role !== "admin") {
    const [tag] = await db
      .select({ idTag: idTag.idTag })
      .from(idTag)
      .where(and(eq(idTag.idTag, body.idTag.trim()), eq(idTag.userId, currentUser.id)))
      .limit(1);
    if (!tag) return c.json({ error: "idTag not found or not authorized" }, 403);
  }

  // Reuse the same authorization logic as Authorize/StartTransaction.
  const tagInfo = await resolveIdTagInfo(body.idTag.trim());
  if (tagInfo.status !== "Accepted") {
    if (tagInfo.status === "ConcurrentTx") {
      return c.json({ error: "ConcurrentTx: idTag already has an active transaction" }, 409);
    }
    return c.json({ error: `idTag rejected: ${tagInfo.status}` }, 400);
  }

  // One idTag can only have one active transaction at a time.
  const [activeTx] = await db
    .select({ id: transaction.id })
    .from(transaction)
    .where(and(eq(transaction.idTag, body.idTag.trim()), isNull(transaction.stopTimestamp)))
    .limit(1);
  if (activeTx) {
    return c.json({ error: "ConcurrentTx: idTag already has an active transaction" }, 409);
  }

  // Verify charge point exists and is Accepted
  const [cp] = await db
    .select({ id: chargePoint.id, registrationStatus: chargePoint.registrationStatus })
    .from(chargePoint)
    .where(eq(chargePoint.chargePointIdentifier, body.chargePointIdentifier.trim()))
    .limit(1);

  if (!cp) return c.json({ error: "ChargePoint not found" }, 404);
  if (cp.registrationStatus !== "Accepted") {
    return c.json({ error: "ChargePoint is not accepted" }, 400);
  }

  try {
    const response = await sendOcppCall<
      { connectorId: number; idTag: string },
      { status?: string }
    >(body.chargePointIdentifier.trim(), "RemoteStartTransaction", {
      connectorId: body.connectorId,
      idTag: body.idTag.trim(),
    })

    if (response?.status && response.status !== "Accepted") {
      return c.json({ error: `RemoteStartTransaction ${response.status}` }, 409)
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : "CommandSendFailed"
    if (message === "TransportUnavailable") {
      return c.json({ error: "ChargePoint command channel is unavailable" }, 503)
    }
    if (message === "CommandTimeout") {
      return c.json({ error: "ChargePoint did not confirm RemoteStartTransaction in time" }, 504)
    }
    return c.json({ error: `RemoteStartTransaction failed: ${message}` }, 502)
  }

  console.log(
    `[OCPP] RemoteStartTransaction cp=${body.chargePointIdentifier} ` +
      `connector=${body.connectorId} idTag=${body.idTag} user=${currentUser.id}`,
  );

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

/** GET /api/transactions?page=1&limit=20&status=active|completed&chargePointId=... */
app.get("/", async (c) => {
  const page = Math.max(1, Number(c.req.query("page") ?? 1));
  const limit = Math.min(100, Math.max(1, Number(c.req.query("limit") ?? 20)));
  const status = c.req.query("status"); // 'active' | 'completed'
  const chargePointId = c.req.query("chargePointId");
  const offset = (page - 1) * limit;

  const db = useDrizzle();
  const currentUser = c.get("user");
  const isAdmin = currentUser?.role === "admin";

  const statusCondition =
    status === "active"
      ? isNull(transaction.stopTimestamp)
      : status === "completed"
        ? isNotNull(transaction.stopTimestamp)
        : undefined;

  // For non-admin users, restrict to transactions matching their id-tags
  const userCondition =
    !isAdmin && currentUser
      ? sql`${transaction.idTag} in (select id_tag from id_tag where user_id = ${currentUser.id})`
      : undefined;

  const cpCondition = chargePointId ? eq(transaction.chargePointId, chargePointId) : undefined;

  const whereClause = and(statusCondition, userCondition, cpCondition);

  const [{ total }] = await db
    .select({ total: sql<number>`count(*)::int` })
    .from(transaction)
    .where(whereClause);

  const rows = await db
    .select({
      transaction,
      chargePointIdentifier: chargePoint.chargePointIdentifier,
      chargePointDeviceName: chargePoint.deviceName,
      feePerKwh: chargePoint.feePerKwh,
      pricingMode: chargePoint.pricingMode,
      connectorNumber: connector.connectorId,
      idTagUserId: idTag.userId,
      idTagUserName: user.name,
    })
    .from(transaction)
    .leftJoin(chargePoint, eq(transaction.chargePointId, chargePoint.id))
    .leftJoin(connector, eq(transaction.connectorId, connector.id))
    .leftJoin(idTag, eq(transaction.idTag, idTag.idTag))
    .leftJoin(user, eq(idTag.userId, user.id))
    .where(whereClause)
    .orderBy(desc(transaction.startTimestamp))
    .limit(limit)
    .offset(offset);

  // For active transactions, fetch the latest meter reading to show live energy
  const activeTxIds = rows.filter((r) => !r.transaction.stopTimestamp).map((r) => r.transaction.id);

  // Map: transactionId -> latest cumulative Wh from meterValue
  const latestMeterMap = new Map<number, number>();
  if (activeTxIds.length > 0) {
    // DISTINCT ON picks the most recent row per transaction_id
    const latestRows = await db.execute<{
      transaction_id: number;
      sampled_values: SampledValue[];
    }>(sql`
      SELECT DISTINCT ON (transaction_id) transaction_id, sampled_values
      FROM meter_value
      WHERE transaction_id IN (${sql.join(
        activeTxIds.map((id) => sql`${id}`),
        sql`, `,
      )})
      ORDER BY transaction_id, timestamp DESC
    `);
    for (const row of latestRows.rows) {
      const svList = row.sampled_values as SampledValue[];
      const energySv = svList.find(
        (sv) => (!sv.measurand || sv.measurand === "Energy.Active.Import.Register") && !sv.phase,
      );
      if (energySv != null) {
        // Unit defaults to Wh; kWh is also common
        const raw = parseFloat(energySv.value);
        if (!Number.isNaN(raw)) {
          const wh = energySv.unit === "kWh" ? raw * 1000 : raw;
          latestMeterMap.set(Number(row.transaction_id), wh);
        }
      }
    }
  }

  return c.json({
    data: rows.map((r) => {
      const isActive = !r.transaction.stopTimestamp;
      const latestMeterWh = isActive ? (latestMeterMap.get(r.transaction.id) ?? null) : null;
      const liveEnergyWh =
        latestMeterWh != null ? latestMeterWh - r.transaction.startMeterValue : null;

      // Estimated cost: only for fixed pricing (TOU requires full interval analysis)
      let estimatedCost: number | null = null;
      if (
        isActive &&
        liveEnergyWh != null &&
        liveEnergyWh > 0 &&
        r.pricingMode === "fixed" &&
        (r.feePerKwh ?? 0) > 0
      ) {
        estimatedCost = Math.ceil((liveEnergyWh * (r.feePerKwh ?? 0)) / 1000);
      }

      return {
        ...r.transaction,
        chargePointIdentifier: r.chargePointIdentifier,
        chargePointDeviceName: r.chargePointDeviceName,
        connectorNumber: r.connectorNumber,
        idTagUserId: r.idTagUserId,
        idTagUserName: r.idTagUserName,
        energyWh:
          r.transaction.stopMeterValue != null
            ? r.transaction.stopMeterValue - r.transaction.startMeterValue
            : null,
        liveEnergyWh,
        estimatedCost,
      };
    }),
    total,
    page,
    totalPages: Math.max(1, Math.ceil(total / limit)),
  });
});

/** GET /api/transactions/:id */
app.get("/:id", async (c) => {
  const db = useDrizzle();
  const id = Number(c.req.param("id"));

  const [row] = await db
    .select({
      transaction,
      chargePointIdentifier: chargePoint.chargePointIdentifier,
      chargePointDeviceName: chargePoint.deviceName,
      connectorNumber: connector.connectorId,
      feePerKwh: chargePoint.feePerKwh,
      pricingMode: chargePoint.pricingMode,
    })
    .from(transaction)
    .leftJoin(chargePoint, eq(transaction.chargePointId, chargePoint.id))
    .leftJoin(connector, eq(transaction.connectorId, connector.id))
    .where(eq(transaction.id, id))
    .limit(1);

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

  let liveEnergyWh: number | null = null;
  let estimatedCost: number | null = null;

  // For active transactions, return live estimated energy/cost like the list endpoint.
  if (!row.transaction.stopTimestamp) {
    const latestRows = await db.execute<{
      sampled_values: SampledValue[];
    }>(sql`
      SELECT sampled_values
      FROM meter_value
      WHERE transaction_id = ${id}
      ORDER BY timestamp DESC
      LIMIT 1
    `);

    const latest = latestRows.rows[0];
    if (latest) {
      const svList = latest.sampled_values as SampledValue[];
      const energySv = svList.find(
        (sv) => (!sv.measurand || sv.measurand === "Energy.Active.Import.Register") && !sv.phase,
      );

      if (energySv != null) {
        const raw = parseFloat(energySv.value);
        if (!Number.isNaN(raw) && row.transaction.startMeterValue != null) {
          const latestMeterWh = energySv.unit === "kWh" ? raw * 1000 : raw;
          liveEnergyWh = latestMeterWh - row.transaction.startMeterValue;

          if (liveEnergyWh > 0 && row.pricingMode === "fixed" && (row.feePerKwh ?? 0) > 0) {
            estimatedCost = Math.ceil((liveEnergyWh * (row.feePerKwh ?? 0)) / 1000);
          }
        }
      }
    }
  }

  return c.json({
    ...row.transaction,
    chargePointIdentifier: row.chargePointIdentifier,
    chargePointDeviceName: row.chargePointDeviceName,
    connectorNumber: row.connectorNumber,
    energyWh:
      row.transaction.stopMeterValue != null
        ? row.transaction.stopMeterValue - row.transaction.startMeterValue
        : null,
    liveEnergyWh,
    estimatedCost,
  });
});

/**
 * POST /api/transactions/:id/stop
 * Manually stop an active transaction.
 * 1. If the charge point is connected, send OCPP RemoteStopTransaction and wait for confirmation.
 * 2. Record the stop request state in DB; final settlement still happens on StopTransaction.
 */
app.post("/:id/stop", async (c) => {
  const db = useDrizzle();
  const id = Number(c.req.param("id"));

  // Load the transaction
  const [row] = await db
    .select({
      transaction,
      chargePointIdentifier: chargePoint.chargePointIdentifier,
    })
    .from(transaction)
    .leftJoin(chargePoint, eq(transaction.chargePointId, chargePoint.id))
    .where(eq(transaction.id, id))
    .limit(1);

  if (!row) return c.json({ error: "Not found" }, 404);
  if (row.transaction.stopTimestamp) return c.json({ error: "Transaction already stopped" }, 409);

  const now = dayjs();

  let remoteStopStatus: "Requested" | "Accepted" | "Rejected" | "Error" | "Timeout" = "Requested";
  let remoteStopRequestId: string | null = null;
  let online = false;

  if (row.chargePointIdentifier) {
    remoteStopRequestId = crypto.randomUUID();
    try {
      const response = await sendOcppCall<{ transactionId: number }, { status?: string }>(
        row.chargePointIdentifier,
        "RemoteStopTransaction",
        { transactionId: row.transaction.id },
        { uniqueId: remoteStopRequestId },
      )
      online = true;
      remoteStopStatus = response?.status === "Accepted" ? "Accepted" : "Rejected";
      console.log(`[OCPP] RemoteStopTransaction txId=${id} status=${response?.status ?? "unknown"} to ${row.chargePointIdentifier}`);
    } catch (error) {
      const message = error instanceof Error ? error.message : "CommandSendFailed";
      remoteStopStatus = message === "CommandTimeout" ? "Timeout" : "Error";
      online = message !== "TransportUnavailable";
      console.warn(`[OCPP] RemoteStopTransaction txId=${id} failed for ${row.chargePointIdentifier}: ${message}`);
      if (message === "TransportUnavailable") {
        return c.json(
          {
            error: "ChargePoint command channel is unavailable",
            online: false,
            remoteStopStatus,
          },
          503,
        );
      }
      if (message === "CommandTimeout") {
        return c.json(
          {
            error: "ChargePoint did not confirm RemoteStopTransaction in time",
            online: true,
            remoteStopStatus,
          },
          504,
        );
      }
      return c.json(
        {
          error: `RemoteStopTransaction failed: ${message}`,
          online,
          remoteStopStatus,
        },
        502,
      );
    }
  }

  const [updated] = await db
    .update(transaction)
    .set({
      remoteStopRequestedAt: now.toDate(),
      remoteStopRequestId,
      remoteStopStatus,
      updatedAt: now.toDate(),
    })
    .where(eq(transaction.id, id))
    .returning();

  return c.json({
    ...updated,
    chargePointIdentifier: row.chargePointIdentifier,
    online,
    remoteStopStatus,
  });
});

/** DELETE /api/transactions/:id  —  delete a transaction record */
app.delete("/:id", async (c) => {
  if (c.get("user")?.role !== "admin") return c.json({ error: "Forbidden" }, 403);
  const db = useDrizzle();
  const id = Number(c.req.param("id"));

  const [row] = await db
    .select({ transaction, connectorId: transaction.connectorId })
    .from(transaction)
    .where(eq(transaction.id, id))
    .limit(1);

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

  // If the transaction is still active, reset the connector to Available
  if (!row.transaction.stopTimestamp) {
    await db
      .update(connector)
      .set({ status: "Available", updatedAt: dayjs().toDate() })
      .where(eq(connector.id, row.transaction.connectorId));
  }

  await db.delete(transaction).where(eq(transaction.id, id));

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

export default app;
