import { schema, table, t, SenderError } from "spacetimedb/server";

// Better Auth OIDC configuration — must match the BETTER_AUTH_URL env var of the auth server.
// SpacetimeDB modules don't have access to process.env, so this is hardcoded.
const BETTER_AUTH_ISSUER = "http://localhost:4000";
const OIDC_AUDIENCE = BETTER_AUTH_ISSUER;

const STARTER_SYSTEM_ID = "solace";
const STARTER_STATION_ID = "solace-prime";
const STARTER_STATION_POI_ID = "poi-solace-prime";
const STARTER_BELT_POI_ID = "poi-solace-belt-alpha";
const APPROACH_DURATION_MS = 5000n;
const MINING_DURATION_MS = 6000n;
const STARTER_WALLET_ISK = 25000n;
const STARTER_CARGO_CAPACITY = 2500n;
const MINING_YIELD_QUANTITY = 1000n;
const VELDSPAR_ITEM_ID = "ore-veldspar";
const VELDSPAR_ITEM_NAME = "Veldspar";
const VELDSPAR_UNIT_PRICE = 12n;

const player = table(
  { name: "player", public: true },
  {
    identity: t.identity().primaryKey(),
    display_name: t.string(),
    created_at: t.timestamp(),
    updated_at: t.timestamp(),
    last_connected_at: t.timestamp(),
    is_connected: t.bool(),
  },
);

const ship = table(
  { name: "ship", public: true },
  {
    ship_id: t.u64().primaryKey().autoInc(),
    owner_identity: t.identity().index("btree"),
    ship_name: t.string(),
    hull_type: t.string(),
    current_system_id: t.string(),
    docked_station_id: t.string(),
    current_poi_id: t.string(),
    selected_poi_id: t.string(),
    flight_mode: t.string(),
    x: t.f32(),
    y: t.f32(),
    z: t.f32(),
    cargo_capacity: t.u64(),
  },
);

const system = table(
  { name: "system", public: true },
  {
    system_id: t.string().primaryKey(),
    name: t.string(),
    security_level: t.f32(),
  },
);

const station = table(
  { name: "station", public: true },
  {
    station_id: t.string().primaryKey(),
    system_id: t.string().index("btree"),
    name: t.string(),
  },
);

const point_of_interest = table(
  { name: "point_of_interest", public: true },
  {
    poi_id: t.string().primaryKey(),
    system_id: t.string().index("btree"),
    name: t.string(),
    poi_type: t.string(),
    x: t.f32(),
    y: t.f32(),
    z: t.f32(),
    station_id: t.string(),
  },
);

const cargo_item = table(
  { name: "cargo_item", public: true },
  {
    cargo_item_id: t.u64().primaryKey().autoInc(),
    owner_identity: t.identity().index("btree"),
    ship_id: t.u64().index("btree"),
    item_id: t.string(),
    item_name: t.string(),
    category: t.string(),
    quantity: t.u64(),
    unit_price: t.u64(),
  },
);

const wallet = table(
  { name: "wallet", public: true },
  {
    owner_identity: t.identity().primaryKey(),
    isk: t.u64(),
    updated_at: t.timestamp(),
  },
);

const ship_operation = table(
  { name: "ship_operation", public: true },
  {
    ship_id: t.u64().primaryKey(),
    operation_type: t.string(),
    target_poi_id: t.string(),
    started_at: t.timestamp(),
    duration_ms: t.u64(),
    completes_at_ms: t.u64(),
  },
);

const server_event = table(
  { name: "server_event", public: true },
  {
    event_id: t.u64().primaryKey().autoInc(),
    at: t.timestamp(),
    actor_identity: t.identity().index("btree"),
    event_type: t.string(),
    message: t.string(),
  },
);

const spacetimedb = schema({
  player,
  ship,
  system,
  station,
  point_of_interest,
  cargo_item,
  wallet,
  ship_operation,
  server_event,
});
export default spacetimedb;

// ── OIDC Authentication ────────────────────────────────────────────────────────
// Validate incoming JWTs from Better Auth when clients connect.
// SpacetimeDB decodes the JWT automatically; we verify issuer & audience.
export const onConnect = spacetimedb.clientConnected((ctx) => {
  const jwt = ctx.senderAuth.jwt;

  if (jwt == null) {
    throw new SenderError("Unauthorized: JWT is required to connect.");
  }

  // Restrict to tokens issued by our Better Auth server
  if (jwt.issuer !== BETTER_AUTH_ISSUER) {
    throw new SenderError(`Unauthorized: Invalid issuer '${jwt.issuer}'.`);
  }

  // Verify the audience matches our application
  if (!jwt.audience.some((aud) => aud === OIDC_AUDIENCE)) {
    throw new SenderError(`Unauthorized: Invalid audience '${jwt.audience}'.`);
  }

  // Client authenticated successfully — sub: jwt.subject, iss: jwt.issuer
});

export const seedWorld = spacetimedb.reducer({}, (ctx) => {
  seedStarterWorld(ctx);
  writeEvent(ctx, "world_seeded", "Starter system and station are available.");
});

export const connectPlayer = spacetimedb.reducer({ displayName: t.string() }, (ctx, { displayName }) => {
  seedStarterWorld(ctx);

  const trimmedName = normalizeDisplayName(displayName);
  const existingPlayer = ctx.db.player.identity.find(ctx.sender);
  const timestamp = ctx.timestamp;

  if (existingPlayer) {
    ctx.db.player.identity.update({
      ...existingPlayer,
      display_name: trimmedName,
      updated_at: timestamp,
      last_connected_at: timestamp,
      is_connected: true,
    });
  } else {
    ctx.db.player.insert({
      identity: ctx.sender,
      display_name: trimmedName,
      created_at: timestamp,
      updated_at: timestamp,
      last_connected_at: timestamp,
      is_connected: true,
    });
  }

  const existingShip = findStarterShip(ctx, ctx.sender);
  if (!existingShip) {
    ctx.db.ship.insert({
      ship_id: 0n,
      owner_identity: ctx.sender,
      ship_name: `${trimmedName}'s Ibis`,
      hull_type: "Starter Frigate",
      current_system_id: STARTER_SYSTEM_ID,
      docked_station_id: STARTER_STATION_ID,
      current_poi_id: STARTER_STATION_POI_ID,
      selected_poi_id: "",
      flight_mode: "docked",
      x: 0,
      y: 0,
      z: 0,
      cargo_capacity: STARTER_CARGO_CAPACITY,
    });
  }

  if (!ctx.db.wallet.owner_identity.find(ctx.sender)) {
    ctx.db.wallet.insert({
      owner_identity: ctx.sender,
      isk: STARTER_WALLET_ISK,
      updated_at: timestamp,
    });
  }

  writeEvent(ctx, "player_connected", `${trimmedName} connected to VOID::NAV.`);
});

export const renamePlayer = spacetimedb.reducer({ displayName: t.string() }, (ctx, { displayName }) => {
  const existingPlayer = ctx.db.player.identity.find(ctx.sender);
  const trimmedName = normalizeDisplayName(displayName);

  if (!existingPlayer) {
    throw new Error("Player must connect before renaming.");
  }

  ctx.db.player.identity.update({
    ...existingPlayer,
    display_name: trimmedName,
    updated_at: ctx.timestamp,
  });

  writeEvent(ctx, "player_renamed", `Pilot renamed to ${trimmedName}.`);
});

export const ping = spacetimedb.reducer({}, (ctx) => {
  writeEvent(ctx, "ping", "Client ping reached the SpacetimeDB module.");
});

export const undock = spacetimedb.reducer({}, (ctx) => {
  const ship = requirePlayerShip(ctx);

  if (ship.docked_station_id.length === 0) {
    throw new Error("Ship is already undocked.");
  }

  ctx.db.ship.ship_id.update({
    ...ship,
    docked_station_id: "",
    flight_mode: "flight",
  });

  writeEvent(ctx, "ship_undocked", `${ship.ship_name} undocked from ${ship.docked_station_id}.`);
});

export const selectTarget = spacetimedb.reducer({ poiId: t.string() }, (ctx, { poiId }) => {
  const ship = requirePlayerShip(ctx);
  const poi = requirePoiInCurrentSystem(ctx, ship, poiId);

  ctx.db.ship.ship_id.update({
    ...ship,
    selected_poi_id: poi.poi_id,
  });

  writeEvent(ctx, "target_selected", `${ship.ship_name} selected ${poi.name}.`);
});

export const startApproach = spacetimedb.reducer({}, (ctx) => {
  const ship = requirePlayerShip(ctx);

  if (ship.docked_station_id.length > 0) {
    throw new Error("Ship must be undocked before approaching a target.");
  }

  if (ship.selected_poi_id.length === 0) {
    throw new Error("Select a target before starting approach.");
  }

  const target = requirePoiInCurrentSystem(ctx, ship, ship.selected_poi_id);
  if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) {
    throw new Error("Ship already has an active operation.");
  }

  const nowMs = ctx.timestamp.toMillis();
  ctx.db.ship_operation.insert({
    ship_id: ship.ship_id,
    operation_type: "approach",
    target_poi_id: target.poi_id,
    started_at: ctx.timestamp,
    duration_ms: APPROACH_DURATION_MS,
    completes_at_ms: nowMs + APPROACH_DURATION_MS,
  });

  ctx.db.ship.ship_id.update({
    ...ship,
    flight_mode: "approaching",
  });

  writeEvent(ctx, "approach_started", `${ship.ship_name} started approach to ${target.name}.`);
});

export const completeApproach = spacetimedb.reducer({}, (ctx) => {
  const ship = requirePlayerShip(ctx);
  const operation = requireOperation(ctx, ship.ship_id, "approach");
  requireElapsed(ctx, operation.completes_at_ms, "Approach is still in progress.");
  const target = requirePoiInCurrentSystem(ctx, ship, operation.target_poi_id);

  ctx.db.ship_operation.ship_id.delete(ship.ship_id);
  ctx.db.ship.ship_id.update({
    ...ship,
    current_poi_id: target.poi_id,
    flight_mode: "flight",
    x: target.x,
    y: target.y,
    z: target.z,
  });

  writeEvent(ctx, "approach_completed", `${ship.ship_name} arrived at ${target.name}.`);
});

export const dock = spacetimedb.reducer({}, (ctx) => {
  const ship = requirePlayerShip(ctx);

  if (ship.docked_station_id.length > 0) {
    throw new Error("Ship is already docked.");
  }

  if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) {
    throw new Error("Cannot dock while an operation is active.");
  }

  const currentPoi = requireCurrentPoi(ctx, ship);
  if (currentPoi.station_id.length === 0) {
    throw new Error("Current point of interest is not dockable.");
  }

  ctx.db.ship.ship_id.update({
    ...ship,
    docked_station_id: currentPoi.station_id,
    flight_mode: "docked",
  });

  writeEvent(ctx, "ship_docked", `${ship.ship_name} docked at ${currentPoi.name}.`);
});

export const startMining = spacetimedb.reducer({}, (ctx) => {
  const ship = requirePlayerShip(ctx);

  if (ship.docked_station_id.length > 0) {
    throw new Error("Ship must be undocked before mining.");
  }

  if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) {
    throw new Error("Ship already has an active operation.");
  }

  const currentPoi = requireCurrentPoi(ctx, ship);
  if (currentPoi.poi_type !== "asteroid_belt") {
    throw new Error("Mining requires being at an asteroid belt.");
  }

  const freeCapacity = getFreeCargoCapacity(ctx, ship);
  if (freeCapacity <= 0n) {
    throw new Error("Cargo hold is full.");
  }

  const nowMs = ctx.timestamp.toMillis();
  ctx.db.ship_operation.insert({
    ship_id: ship.ship_id,
    operation_type: "mining",
    target_poi_id: currentPoi.poi_id,
    started_at: ctx.timestamp,
    duration_ms: MINING_DURATION_MS,
    completes_at_ms: nowMs + MINING_DURATION_MS,
  });

  ctx.db.ship.ship_id.update({
    ...ship,
    flight_mode: "mining",
  });

  writeEvent(ctx, "mining_started", `${ship.ship_name} started mining ${currentPoi.name}.`);
});

export const completeMiningCycle = spacetimedb.reducer({}, (ctx) => {
  const ship = requirePlayerShip(ctx);
  const operation = requireOperation(ctx, ship.ship_id, "mining");
  requireElapsed(ctx, operation.completes_at_ms, "Mining cycle is still in progress.");

  const currentPoi = requireCurrentPoi(ctx, ship);
  if (currentPoi.poi_type !== "asteroid_belt") {
    throw new Error("Mining can only complete at an asteroid belt.");
  }

  const freeCapacity = getFreeCargoCapacity(ctx, ship);
  if (freeCapacity <= 0n) {
    throw new Error("Cargo hold is full.");
  }

  const quantity = minBigint(MINING_YIELD_QUANTITY, freeCapacity);
  const existingCargo = findCargoItem(ctx, ship.ship_id, VELDSPAR_ITEM_ID);

  if (existingCargo) {
    ctx.db.cargo_item.cargo_item_id.update({
      ...existingCargo,
      quantity: existingCargo.quantity + quantity,
      unit_price: VELDSPAR_UNIT_PRICE,
    });
  } else {
    ctx.db.cargo_item.insert({
      cargo_item_id: 0n,
      owner_identity: ctx.sender,
      ship_id: ship.ship_id,
      item_id: VELDSPAR_ITEM_ID,
      item_name: VELDSPAR_ITEM_NAME,
      category: "ore",
      quantity,
      unit_price: VELDSPAR_UNIT_PRICE,
    });
  }

  ctx.db.ship_operation.ship_id.delete(ship.ship_id);
  ctx.db.ship.ship_id.update({
    ...ship,
    flight_mode: "flight",
  });

  writeEvent(ctx, "mining_completed", `${ship.ship_name} mined ${quantity.toString()} ${VELDSPAR_ITEM_NAME}.`);
});

export const sellOreToNpcMarket = spacetimedb.reducer(
  { itemId: t.string(), quantity: t.u64() },
  (ctx, { itemId, quantity }) => {
    const ship = requirePlayerShip(ctx);

    if (ship.docked_station_id.length === 0) {
      throw new Error("Ship must be docked before selling ore.");
    }

    if (quantity <= 0n) {
      throw new Error("Sell quantity must be greater than zero.");
    }

    const cargo = findCargoItem(ctx, ship.ship_id, itemId);
    if (!cargo || cargo.category !== "ore") {
      throw new Error("Ore cargo item not found.");
    }

    if (cargo.quantity < quantity) {
      throw new Error("Cannot sell more ore than the ship is carrying.");
    }

    const playerWallet = ctx.db.wallet.owner_identity.find(ctx.sender);
    if (!playerWallet) {
      throw new Error("Player wallet was not found.");
    }

    if (cargo.quantity === quantity) {
      ctx.db.cargo_item.cargo_item_id.delete(cargo.cargo_item_id);
    } else {
      ctx.db.cargo_item.cargo_item_id.update({
        ...cargo,
        quantity: cargo.quantity - quantity,
      });
    }

    const payout = quantity * cargo.unit_price;
    ctx.db.wallet.owner_identity.update({
      ...playerWallet,
      isk: playerWallet.isk + payout,
      updated_at: ctx.timestamp,
    });

    writeEvent(ctx, "ore_sold", `Sold ${quantity.toString()} ${cargo.item_name} for ${payout.toString()} ISK.`);
  },
);

function seedStarterWorld(ctx: ReducerContextLike) {
  if (!ctx.db.system.system_id.find(STARTER_SYSTEM_ID)) {
    ctx.db.system.insert({
      system_id: STARTER_SYSTEM_ID,
      name: "Solace",
      security_level: 0.9,
    });
  }

  if (!ctx.db.station.station_id.find(STARTER_STATION_ID)) {
    ctx.db.station.insert({
      station_id: STARTER_STATION_ID,
      system_id: STARTER_SYSTEM_ID,
      name: "Solace Prime Orbital",
    });
  }

  if (!ctx.db.point_of_interest.poi_id.find(STARTER_STATION_POI_ID)) {
    ctx.db.point_of_interest.insert({
      poi_id: STARTER_STATION_POI_ID,
      system_id: STARTER_SYSTEM_ID,
      name: "Solace Prime Orbital",
      poi_type: "station",
      x: 0,
      y: 0,
      z: 0,
      station_id: STARTER_STATION_ID,
    });
  }

  if (!ctx.db.point_of_interest.poi_id.find(STARTER_BELT_POI_ID)) {
    ctx.db.point_of_interest.insert({
      poi_id: STARTER_BELT_POI_ID,
      system_id: STARTER_SYSTEM_ID,
      name: "Solace Belt Alpha",
      poi_type: "asteroid_belt",
      x: 34,
      y: -2,
      z: -18,
      station_id: "",
    });
  }
}

function findStarterShip(ctx: ReducerContextLike, ownerIdentity: unknown) {
  return Array.from(ctx.db.ship.iter()).find((row) => identitiesEqual(row.owner_identity, ownerIdentity));
}

function requirePlayerShip(ctx: ReducerContextLike) {
  const playerShip = findStarterShip(ctx, ctx.sender);
  if (!playerShip) {
    throw new Error("Player must connect before controlling a ship.");
  }
  return playerShip;
}

function requirePoiInCurrentSystem(ctx: ReducerContextLike, ship: ReturnType<typeof requirePlayerShip>, poiId: string) {
  const poi = ctx.db.point_of_interest.poi_id.find(poiId);
  if (!poi) {
    throw new Error(`Point of interest not found: ${poiId}.`);
  }

  if (poi.system_id !== ship.current_system_id) {
    throw new Error("Selected point of interest is not in the ship's current system.");
  }

  return poi;
}

function requireCurrentPoi(ctx: ReducerContextLike, ship: ReturnType<typeof requirePlayerShip>) {
  if (ship.current_poi_id.length === 0) {
    throw new Error("Ship is not at a point of interest.");
  }

  return requirePoiInCurrentSystem(ctx, ship, ship.current_poi_id);
}

function requireOperation(ctx: ReducerContextLike, shipId: bigint, operationType: string) {
  const operation = ctx.db.ship_operation.ship_id.find(shipId);
  if (!operation) {
    throw new Error(`No active ${operationType} operation found.`);
  }

  if (operation.operation_type !== operationType) {
    throw new Error(`Active operation is ${operation.operation_type}, not ${operationType}.`);
  }

  return operation;
}

function requireElapsed(ctx: ReducerContextLike, completesAtMs: bigint, message: string) {
  if (ctx.timestamp.toMillis() < completesAtMs) {
    throw new Error(message);
  }
}

function findCargoItem(ctx: ReducerContextLike, shipId: bigint, itemId: string) {
  return Array.from(ctx.db.cargo_item.iter()).find((row) => row.ship_id === shipId && row.item_id === itemId);
}

function getShipCargoQuantity(ctx: ReducerContextLike, shipId: bigint) {
  return Array.from(ctx.db.cargo_item.iter())
    .filter((row) => row.ship_id === shipId)
    .reduce((total, row) => total + row.quantity, 0n);
}

function getFreeCargoCapacity(ctx: ReducerContextLike, ship: ReturnType<typeof requirePlayerShip>) {
  const usedCapacity = getShipCargoQuantity(ctx, ship.ship_id);
  return ship.cargo_capacity > usedCapacity ? ship.cargo_capacity - usedCapacity : 0n;
}

function minBigint(left: bigint, right: bigint) {
  return left < right ? left : right;
}

function identitiesEqual(left: unknown, right: unknown) {
  if (left && right && typeof left === "object" && "isEqual" in left && typeof left.isEqual === "function") {
    return left.isEqual(right);
  }
  return left === right;
}

function writeEvent(ctx: ReducerContextLike, eventType: string, message: string) {
  ctx.db.server_event.insert({
    event_id: 0n,
    at: ctx.timestamp,
    actor_identity: ctx.sender,
    event_type: eventType,
    message,
  });
}

function normalizeDisplayName(displayName: string) {
  const trimmedName = displayName.trim();
  return trimmedName.length > 0 ? trimmedName.slice(0, 32) : "New Pilot";
}

type ReducerContextLike = Parameters<Parameters<typeof spacetimedb.reducer>[1]>[0];
