import { asc, eq, sql } from "drizzle-orm";
import dayjs from "dayjs";
import { useDrizzle } from "@/lib/db.js";
import { chargePoint, connector, idTag, meterValue, transaction } from "@/db/schema.js";
import { tariff } from "@/db/schema.js";
import type { SampledValue } from "@/db/schema.js";
import type { TariffSlot, PriceTier } from "@/db/tariff-schema.ts";
import type {
  StopTransactionRequest,
  StopTransactionResponse,
  OcppConnectionContext,
} from "../types.ts";
import { resolveIdTagInfo } from "./authorize.ts";

export async function handleStopTransaction(
  payload: StopTransactionRequest,
  _ctx: OcppConnectionContext,
): Promise<StopTransactionResponse> {
  const db = useDrizzle();

  // Update the transaction record
  const [tx] = await db
    .update(transaction)
    .set({
      stopTimestamp: dayjs(payload.timestamp).toDate(),
      stopMeterValue: payload.meterStop,
      stopIdTag: payload.idTag ?? null,
      stopReason: (payload.reason as (typeof transaction.$inferSelect)["stopReason"]) ?? null,
      remoteStopStatus: sql`case
        when ${transaction.remoteStopRequestedAt} is not null and coalesce(${transaction.remoteStopStatus}, 'Requested') in ('Requested', 'Accepted')
          then 'Accepted'
        else ${transaction.remoteStopStatus}
      end`,
      updatedAt: dayjs().toDate(),
    })
    .where(eq(transaction.id, payload.transactionId))
    .returning();

  if (!tx) {
    console.warn(`[OCPP] StopTransaction: transaction ${payload.transactionId} not found`);
    return {};
  }

  // Set connector back to Available
  await db
    .update(connector)
    .set({ status: "Available", updatedAt: dayjs().toDate() })
    .where(eq(connector.id, tx.connectorId));

  // Store embedded meter values (transactionData)
  if (payload.transactionData?.length) {
    const records = payload.transactionData.flatMap((mv) =>
      mv.sampledValue?.length
        ? [
            {
              id: crypto.randomUUID(),
              transactionId: tx.id,
              connectorId: tx.connectorId,
              chargePointId: tx.chargePointId,
              connectorNumber: tx.connectorNumber,
              timestamp: dayjs(mv.timestamp).toDate(),
              sampledValues:
                mv.sampledValue as unknown as (typeof meterValue.$inferInsert)["sampledValues"],
            },
          ]
        : [],
    );
    if (records.length) {
      await db.insert(meterValue).values(records);
    }
  }

  const energyWh = payload.meterStop - tx.startMeterValue;

  // Load active tariff and charge point fee
  const [cp] = await db
    .select({ feePerKwh: chargePoint.feePerKwh, pricingMode: chargePoint.pricingMode })
    .from(chargePoint)
    .where(eq(chargePoint.id, tx.chargePointId))
    .limit(1);

  const [activeTariff] = await db.select().from(tariff).where(eq(tariff.isActive, true)).limit(1);

  let electricityFen: number | null = null;
  let serviceFeeFen: number | null = null;
  let feeFen = 0;

  if (activeTariff && cp?.pricingMode === 'tou' && energyWh > 0 && tx.startTimestamp) {
    const stopTs = dayjs(payload.timestamp).toDate();
    const slots = activeTariff.slots as TariffSlot[];

    const priceMap: Record<PriceTier, { electricity: number; service: number }> = {
      peak: {
        electricity: activeTariff.peakElectricityPrice,
        service: activeTariff.peakServiceFee,
      },
      valley: {
        electricity: activeTariff.valleyElectricityPrice,
        service: activeTariff.valleyServiceFee,
      },
      flat: {
        electricity: activeTariff.flatElectricityPrice,
        service: activeTariff.flatServiceFee,
      },
    };

    // Build checkpoints from intermediate meter value readings.
    // Each checkpoint is (timestamp, cumulativeWh), sorted ascending.
    // Using actual intermediate readings lets each interval's known energy delta
    // be assigned to its true time slot rather than assuming uniform charging rate.
    const intervalReadings = await db
      .select({ timestamp: meterValue.timestamp, sampledValues: meterValue.sampledValues })
      .from(meterValue)
      .where(eq(meterValue.transactionId, tx.id))
      .orderBy(asc(meterValue.timestamp));

    const checkpoints: { ts: Date; wh: number }[] = [
      { ts: tx.startTimestamp, wh: tx.startMeterValue },
    ];
    for (const mv of intervalReadings) {
      const svList = mv.sampledValues as SampledValue[];
      // Per OCPP 1.6 §7.17: absent measurand defaults to Energy.Active.Import.Register
      const energySv = svList.find(
        (sv) => (!sv.measurand || sv.measurand === "Energy.Active.Import.Register") && !sv.phase,
      );
      if (!energySv) continue;
      const rawVal = parseFloat(energySv.value);
      if (isNaN(rawVal)) continue;
      const wh = (energySv.unit ?? "Wh") === "kWh" ? rawVal * 1000 : rawVal;
      checkpoints.push({ ts: mv.timestamp, wh });
    }
    checkpoints.push({ ts: stopTs, wh: payload.meterStop });

    // For each interval, compute actual energy delta and distribute across tiers
    // by walking minute-by-minute only within that interval.
    const tierEnergyWh: Record<PriceTier, number> = { peak: 0, valley: 0, flat: 0 };
    for (let i = 0; i + 1 < checkpoints.length; i++) {
      const { ts: t1, wh: wh1 } = checkpoints[i];
      const { ts: t2, wh: wh2 } = checkpoints[i + 1];
      const deltaWh = Math.max(0, wh2 - wh1);
      if (deltaWh === 0) continue;
      const fractions = calcTierFractions(t1, t2, slots);
      for (const tier of ["peak", "valley", "flat"] as PriceTier[]) {
        tierEnergyWh[tier] += deltaWh * fractions[tier];
      }
    }

    let elecFen = 0;
    let svcFen = 0;
    for (const tier of ["peak", "valley", "flat"] as PriceTier[]) {
      const energyKwh = tierEnergyWh[tier] / 1000;
      elecFen += energyKwh * priceMap[tier].electricity;
      svcFen += energyKwh * priceMap[tier].service;
    }
    electricityFen = Math.ceil(elecFen);
    serviceFeeFen = Math.ceil(svcFen);
    feeFen = electricityFen + serviceFeeFen;
  } else if (cp && cp.feePerKwh > 0 && energyWh > 0) {
    // Fallback: flat rate per charge point
    feeFen = Math.ceil((energyWh * cp.feePerKwh) / 1000);
  }

  // Always record the charge amount (0 if free)
  await db
    .update(transaction)
    .set({
      chargeAmount: feeFen,
      electricityFee: electricityFen,
      serviceFee: serviceFeeFen,
      updatedAt: dayjs().toDate(),
    })
    .where(eq(transaction.id, tx.id));

  if (feeFen > 0) {
    await db
      .update(idTag)
      .set({
        balance: sql`${idTag.balance} - ${feeFen}`,
        updatedAt: dayjs().toDate(),
      })
      .where(eq(idTag.idTag, tx.idTag));
  }

  console.log(
    `[OCPP] StopTransaction txId=${payload.transactionId} ` +
      `reason=${payload.reason ?? "none"} energyWh=${energyWh} ` +
      `feeFen=${feeFen} (elec=${electricityFen ?? "flat"} svc=${serviceFeeFen ?? "-"})`,
  );

  // Resolve idTagInfo for the stop tag if provided (no balance check — charging already occurred)
  const idTagInfo = payload.idTag
    ? await resolveIdTagInfo(payload.idTag, false, false)
    : undefined;

  return { idTagInfo };
}

// ── helpers ────────────────────────────────────────────────────────────────

/**
 * Given a charging session's start/stop timestamps and the active tariff slots,
 * returns the fraction of total session duration spent in each price tier.
 * Fractions sum to 1.0.
 *
 * Strategy: walk minute-by-minute over the session and bucket each minute into
 * its tier. Using minutes keeps the implementation simple and introduces at most
 * 1 minute of rounding error (negligible for billing amounts in fen).
 */
function calcTierFractions(
  start: Date,
  stop: Date,
  slots: TariffSlot[],
): Record<PriceTier, number> {
  const totals: Record<PriceTier, number> = { peak: 0, valley: 0, flat: 0 };

  // Build a 24-element hour → tier lookup
  const hourTier: PriceTier[] = [];
  for (let i = 0; i < 24; i++) hourTier.push("flat");
  for (const slot of slots) {
    for (let h = slot.start; h < slot.end; h++) {
      hourTier[h] = slot.tier;
    }
  }

  const startMs = start.getTime();
  const stopMs = stop.getTime();
  const totalMinutes = Math.max(1, Math.round((stopMs - startMs) / 60_000));

  for (let m = 0; m < totalMinutes; m++) {
    const t = new Date(startMs + m * 60_000);
    // 北京时间 = UTC + 8h，峰谷时段以北京时间为准
    const hourBeijing = Math.floor((t.getUTCHours() * 60 + t.getUTCMinutes() + 480) / 60) % 24;
    totals[hourTier[hourBeijing]]++;
  }

  return {
    peak: totals.peak / totalMinutes,
    valley: totals.valley / totalMinutes,
    flat: totals.flat / totalMinutes,
  };
}
