<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">

<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">

<!---
AllocateBusinessBeaconNamespace API
===================================
Allocates or retrieves the beacon namespace (shard UUID + major) for a business.

Request:
  POST /api/beacon-sharding/allocate_business_namespace.cfm
  { "BusinessID": 123 }

Response:
  {
    "OK": true,
    "BusinessID": 123,
    "BeaconShardUUID": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
    "BeaconMajor": 42,
    "ShardID": 1
  }

Logic:
  1. If business already has namespace assigned, return it
  2. Otherwise, find shard with lowest utilization
  3. Assign next available Major within that shard
  4. Increment shard's BusinessCount
--->

<cftry>
<cfscript>
function apiAbort(obj) {
  writeOutput(serializeJSON(obj));
  abort;
}

function readJsonBody() {
  raw = toString(getHttpRequestData().content);
  if (isNull(raw) || len(trim(raw)) EQ 0) return {};
  try {
    parsed = deserializeJSON(raw);
  } catch(any e) {
    apiAbort({ OK=false, ERROR="bad_json", MESSAGE="Invalid JSON body" });
  }
  if (!isStruct(parsed)) return {};
  return parsed;
}

data = readJsonBody();
httpHeaders = getHttpRequestData().headers;

// Get BusinessID from: session > body > X-Business-ID header > URL
bizId = 0;
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID) && request.BusinessID GT 0) {
  bizId = int(request.BusinessID);
}
if (bizId LTE 0 && structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID) && data.BusinessID GT 0) {
  bizId = int(data.BusinessID);
}
if (bizId LTE 0 && structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"]) && httpHeaders["X-Business-ID"] GT 0) {
  bizId = int(httpHeaders["X-Business-ID"]);
}
if (bizId LTE 0 && structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID) && url.BusinessID GT 0) {
  bizId = int(url.BusinessID);
}
if (bizId LTE 0) {
  apiAbort({ OK=false, ERROR="missing_business_id", MESSAGE="BusinessID is required" });
}
</cfscript>

<!--- Verify business exists --->
<cfquery name="qBiz" datasource="payfrit">
  SELECT ID, BeaconShardID, BeaconMajor
  FROM Businesses
  WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
  LIMIT 1
</cfquery>

<cfif qBiz.recordCount EQ 0>
  <cfset apiAbort({ OK=false, ERROR="invalid_business", MESSAGE="Business not found" })>
</cfif>

<!--- If already allocated, return existing namespace --->
<cfif qBiz.BeaconShardID GT 0 AND NOT isNull(qBiz.BeaconMajor)>
  <cfquery name="qShard" datasource="payfrit">
    SELECT UUID FROM BeaconShards WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#qBiz.BeaconShardID#">
  </cfquery>
  <cfoutput>#serializeJSON({
    OK = true,
    BusinessID = bizId,
    BeaconShardUUID = qShard.UUID,
    BeaconMajor = qBiz.BeaconMajor,
    ShardID = qBiz.BeaconShardID,
    AlreadyAllocated = true
  })#</cfoutput>
  <cfabort>
</cfif>

<!--- Find shard with lowest utilization --->
<cfquery name="qShard" datasource="payfrit">
  SELECT ID, UUID, BusinessCount
  FROM BeaconShards
  WHERE IsActive = 1
    AND BusinessCount < MaxBusinesses
  ORDER BY BusinessCount ASC
  LIMIT 1
  FOR UPDATE
</cfquery>

<cfif qShard.recordCount EQ 0>
  <cfset apiAbort({ OK=false, ERROR="no_available_shards", MESSAGE="All beacon shards are at capacity" })>
</cfif>

<cfset shardId = qShard.ID>
<cfset shardUUID = qShard.UUID>

<!--- Find next available Major within this shard --->
<!--- Major values: 0-65535 (uint16), we start from 1 to avoid 0 --->
<cfquery name="qMaxMajor" datasource="payfrit">
  SELECT COALESCE(MAX(BeaconMajor), 0) AS MaxMajor
  FROM Businesses
  WHERE BeaconShardID = <cfqueryparam cfsqltype="cf_sql_integer" value="#shardId#">
</cfquery>

<cfset nextMajor = qMaxMajor.MaxMajor + 1>

<!--- Sanity check --->
<cfif nextMajor GT 65535>
  <cfset apiAbort({ OK=false, ERROR="shard_full", MESSAGE="Shard has reached maximum major value" })>
</cfif>

<!--- Assign namespace to business --->
<cfquery datasource="payfrit">
  UPDATE Businesses
  SET BeaconShardID = <cfqueryparam cfsqltype="cf_sql_integer" value="#shardId#">,
      BeaconMajor = <cfqueryparam cfsqltype="cf_sql_smallint" value="#nextMajor#">
  WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
    AND (BeaconShardID IS NULL OR BeaconMajor IS NULL)
</cfquery>

<!--- Increment shard's business count --->
<cfquery datasource="payfrit">
  UPDATE BeaconShards
  SET BusinessCount = BusinessCount + 1
  WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#shardId#">
</cfquery>

<cfoutput>#serializeJSON({
  OK = true,
  BusinessID = bizId,
  BeaconShardUUID = shardUUID,
  BeaconMajor = nextMajor,
  ShardID = shardId,
  AlreadyAllocated = false
})#</cfoutput>

<cfcatch type="any">
  <cfheader statuscode="200" statustext="OK">
  <cfcontent type="application/json; charset=utf-8" reset="true">
  <cfoutput>#serializeJSON({ OK=false, ERROR="server_error", MESSAGE=cfcatch.message, DETAIL=cfcatch.detail })#</cfoutput>
</cfcatch>
</cftry>
