import { PreciseDate } from "@google-cloud/precise-date";
import { OID } from "@postgresql-typed/oids";
import postgres from "postgres";
import { BigIntJSON } from "../../../shared/src/bigint-json.js";
import { randInt } from "../../../shared/src/rand.js";
import { NUMERIC, DATE, TIME, TIMETZ, TIMESTAMP, TIMESTAMPTZ, JSON, JSONB } from "./pg-types.js";
function timestampToFpMillis(timestamp) {
  timestamp = timestamp.replace(" ", "T");
  const positiveOffset = timestamp.includes("+");
  const tzSplitIndex = positiveOffset ? timestamp.lastIndexOf("+") : timestamp.indexOf("-", timestamp.indexOf("T"));
  const timezoneOffset = tzSplitIndex === -1 ? void 0 : timestamp.substring(tzSplitIndex);
  const tsWithoutTimezone = (tzSplitIndex === -1 ? timestamp : timestamp.substring(0, tzSplitIndex)) + "Z";
  try {
    const fullTime = new PreciseDate(tsWithoutTimezone).getFullTime();
    const millis = Number(fullTime / 1000000n);
    const nanos = Number(fullTime % 1000000n);
    const ret = millis + nanos * 1e-6;
    if (timezoneOffset) {
      const [hours, minutes] = timezoneOffset.split(":");
      const offset = Math.abs(Number(hours)) * 60 + (minutes ? Number(minutes) : 0);
      const offsetMillis = offset * 60 * 1e3;
      return positiveOffset ? ret - offsetMillis : ret + offsetMillis;
    }
    return ret;
  } catch (e) {
    throw new Error(`Error parsing ${timestamp}`, { cause: e });
  }
}
function serializeTimestamp(val) {
  switch (typeof val) {
    case "string":
      return val;
    // Let Postgres parse it
    case "number": {
      if (Number.isInteger(val)) {
        return new PreciseDate(val).toISOString();
      }
      const nanoseconds = 1000000n * BigInt(Math.trunc(val)) + BigInt(Math.trunc(val % 1 * 1e6));
      return new PreciseDate(nanoseconds).toISOString();
    }
    // Note: Don't support bigint inputs until we decide what the semantics are (e.g. micros vs nanos)
    // case 'bigint':
    //   return new PreciseDate(val).toISOString();
    default:
      if (val instanceof Date) {
        return val.toISOString();
      }
  }
  throw new Error(`Unsupported type "${typeof val}" for timestamp: ${val}`);
}
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1e3;
function millisecondsToPostgresTime(milliseconds) {
  if (milliseconds < 0) {
    throw new Error("Milliseconds cannot be negative");
  }
  if (milliseconds >= MILLISECONDS_PER_DAY) {
    throw new Error(
      `Milliseconds cannot exceed 24 hours (${MILLISECONDS_PER_DAY}ms)`
    );
  }
  milliseconds = Math.floor(milliseconds);
  const totalSeconds = Math.floor(milliseconds / 1e3);
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor(totalSeconds % 3600 / 60);
  const seconds = totalSeconds % 60;
  const ms = milliseconds % 1e3;
  return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${ms.toString().padStart(3, "0")}+00`;
}
function postgresTimeToMilliseconds(timeString) {
  if (!timeString || typeof timeString !== "string") {
    throw new Error("Invalid time string: must be a non-empty string");
  }
  const timeRegex = /^(\d{1,2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?(?:([+-])(\d{1,2})(?::(\d{2}))?)?$/;
  const match = timeString.match(timeRegex);
  if (!match) {
    throw new Error(
      `Invalid time format: "${timeString}". Expected HH:MM:SS[.mmm][+|-HH[:MM]]`
    );
  }
  const hours = parseInt(match[1], 10);
  const minutes = parseInt(match[2], 10);
  const seconds = parseInt(match[3], 10);
  let milliseconds = 0;
  if (match[4]) {
    const msString = match[4].padEnd(6, "0");
    milliseconds = parseInt(msString.slice(0, 3), 10);
  }
  if (hours < 0 || hours > 24) {
    throw new Error(
      `Invalid hours: ${hours}. Must be between 0 and 24 (24 means end of day)`
    );
  }
  if (minutes < 0 || minutes >= 60) {
    throw new Error(`Invalid minutes: ${minutes}. Must be between 0 and 59`);
  }
  if (seconds < 0 || seconds >= 60) {
    throw new Error(`Invalid seconds: ${seconds}. Must be between 0 and 59`);
  }
  if (milliseconds < 0 || milliseconds >= 1e3) {
    throw new Error(
      `Invalid milliseconds: ${milliseconds}. Must be between 0 and 999`
    );
  }
  if (hours === 24 && (minutes !== 0 || seconds !== 0 || milliseconds !== 0)) {
    throw new Error(
      "Invalid time: when hours is 24, minutes, seconds, and milliseconds must be 0"
    );
  }
  let totalMs = hours * 36e5 + minutes * 6e4 + seconds * 1e3 + milliseconds;
  if (match[5]) {
    const sign = match[5] === "+" ? 1 : -1;
    const tzHours = parseInt(match[6], 10);
    const tzMinutes = match[7] ? parseInt(match[7], 10) : 0;
    const offsetMs = sign * (tzHours * 36e5 + tzMinutes * 6e4);
    totalMs -= offsetMs;
  }
  if (totalMs > MILLISECONDS_PER_DAY || totalMs < 0) {
    return (totalMs % MILLISECONDS_PER_DAY + MILLISECONDS_PER_DAY) % MILLISECONDS_PER_DAY;
  }
  return totalMs;
}
function dateToUTCMidnight(date) {
  const d = new Date(date);
  return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
}
const postgresTypeConfig = ({ sendStringAsJson } = {}) => ({
  // Type the type IDs as `number` so that Typescript doesn't complain about
  // referencing external types during type inference.
  types: {
    bigint: postgres.BigInt,
    json: {
      to: JSON,
      from: [JSON, JSONB],
      serialize: sendStringAsJson ? (x) => typeof x === "string" ? x : BigIntJSON.stringify(x) : BigIntJSON.stringify,
      parse: BigIntJSON.parse
    },
    // Timestamps are converted to PreciseDate objects.
    timestamp: {
      to: TIMESTAMP,
      from: [TIMESTAMP, TIMESTAMPTZ],
      serialize: serializeTimestamp,
      parse: timestampToFpMillis
    },
    // Times are converted as strings
    time: {
      to: TIME,
      from: [TIME, TIMETZ],
      serialize: (x) => {
        switch (typeof x) {
          case "string":
            return x;
          // Let Postgres parse it
          case "number":
            return millisecondsToPostgresTime(x);
        }
        throw new Error(`Unsupported type "${typeof x}" for time: ${x}`);
      },
      parse: postgresTimeToMilliseconds
    },
    // The DATE type is stored directly as the PG normalized date string.
    date: {
      to: DATE,
      from: [DATE],
      serialize: (x) => (x instanceof Date ? x : new Date(x)).toISOString(),
      parse: dateToUTCMidnight
    },
    // Returns a `js` number which can lose precision for large numbers.
    // JS number is 53 bits so this should generally not occur.
    // An API will be provided for users to override this type.
    numeric: {
      to: NUMERIC,
      from: [NUMERIC],
      serialize: (x) => String(x),
      // pg expects a string
      parse: (x) => Number(x)
    }
  }
});
function pgClient(lc, connectionURI, options, opts) {
  const onnotice = (n) => {
    switch (n.severity) {
      case "NOTICE":
        return;
      // silenced
      case "DEBUG":
        lc.debug?.(n);
        return;
      case "WARNING":
      case "EXCEPTION":
        lc.error?.(n);
        return;
      case "LOG":
      case "INFO":
      default:
        lc.info?.(n);
    }
  };
  const url = new URL(connectionURI);
  const sslFlag = url.searchParams.get("ssl") ?? url.searchParams.get("sslmode") ?? "prefer";
  let ssl;
  if (sslFlag === "disable" || sslFlag === "false") {
    ssl = false;
  } else if (sslFlag === "no-verify") {
    ssl = { rejectUnauthorized: false };
  } else {
    ssl = sslFlag;
  }
  const maxLifetimeSeconds = randInt(5 * 60, 10 * 60);
  return postgres(connectionURI, {
    ...postgresTypeConfig(opts),
    onnotice,
    ["max_lifetime"]: maxLifetimeSeconds,
    ssl,
    ...options
  });
}
Object.freeze(
  Object.fromEntries(
    Object.entries(OID).map(([name, oid]) => [
      oid,
      name.startsWith("_") ? `${name.substring(1)}[]` : name
    ])
  )
);
export {
  millisecondsToPostgresTime,
  pgClient,
  postgresTimeToMilliseconds,
  postgresTypeConfig,
  timestampToFpMillis
};
//# sourceMappingURL=pg.js.map
