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

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

<!---
RegisterBeaconHardware API
==========================
Registers a physical beacon device after provisioning.

Request:
  POST /api/beacon-sharding/register_beacon_hardware.cfm
  {
    "HardwareId": "AA:BB:CC:DD:EE:FF",
    "BusinessID": 123,
    "ServicePointID": 456,
    "UUID": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
    "Major": 42,
    "Minor": 7,
    "TxPower": -59,
    "AdvertisingInterval": 350,
    "FirmwareVersion": "1.2.3"
  }

Response:
  {
    "OK": true,
    "BeaconHardwareID": 1,
    "Status": "assigned"
  }

Logic:
  1. Validate all required fields
  2. Verify business namespace matches UUID/Major
  3. Verify service point Minor matches
  4. Create or update BeaconHardware record
--->

<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;
}

function normStr(v) {
  if (isNull(v)) return "";
  return trim(toString(v));
}

data = readJsonBody();

// Required fields
hardwareId = normStr(structKeyExists(data, "HardwareId") ? data.HardwareId : "");
if (len(hardwareId) EQ 0) {
  apiAbort({ OK=false, ERROR="missing_hardware_id", MESSAGE="HardwareId is required" });
}

bizId = 0;
if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID)) {
  bizId = int(data.BusinessID);
}
if (bizId LTE 0) {
  apiAbort({ OK=false, ERROR="missing_business_id", MESSAGE="BusinessID is required" });
}

spId = 0;
if (structKeyExists(data, "ServicePointID") && isNumeric(data.ServicePointID)) {
  spId = int(data.ServicePointID);
}
if (spId LTE 0) {
  apiAbort({ OK=false, ERROR="missing_servicepoint_id", MESSAGE="ServicePointID is required" });
}

beaconUUID = normStr(structKeyExists(data, "UUID") ? data.UUID : "");
if (len(beaconUUID) EQ 0) {
  apiAbort({ OK=false, ERROR="missing_uuid", MESSAGE="UUID is required" });
}

major = 0;
if (structKeyExists(data, "Major") && isNumeric(data.Major)) {
  major = int(data.Major);
}

minor = 0;
if (structKeyExists(data, "Minor") && isNumeric(data.Minor)) {
  minor = int(data.Minor);
}

// Optional fields
txPower = "";
if (structKeyExists(data, "TxPower") && isNumeric(data.TxPower)) {
  txPower = int(data.TxPower);
}

advInterval = "";
if (structKeyExists(data, "AdvertisingInterval") && isNumeric(data.AdvertisingInterval)) {
  advInterval = int(data.AdvertisingInterval);
}

firmwareVersion = normStr(structKeyExists(data, "FirmwareVersion") ? data.FirmwareVersion : "");
</cfscript>

<!--- Verify business exists and has matching namespace --->
<cfquery name="qBiz" datasource="payfrit">
  SELECT b.ID, b.BeaconShardID, b.BeaconMajor, bs.UUID AS ShardUUID
  FROM Businesses b
  LEFT JOIN BeaconShards bs ON b.BeaconShardID = bs.ID
  WHERE b.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>

<!--- Verify UUID matches business's shard --->
<cfif compareNoCase(qBiz.ShardUUID, beaconUUID) NEQ 0>
  <cfset apiAbort({
    OK=false,
    ERROR="uuid_mismatch",
    MESSAGE="UUID does not match business's assigned shard",
    ExpectedUUID=qBiz.ShardUUID,
    ProvidedUUID=beaconUUID
  })>
</cfif>

<!--- Verify Major matches --->
<cfif qBiz.BeaconMajor NEQ major>
  <cfset apiAbort({
    OK=false,
    ERROR="major_mismatch",
    MESSAGE="Major does not match business's assigned value",
    ExpectedMajor=qBiz.BeaconMajor,
    ProvidedMajor=major
  })>
</cfif>

<!--- Verify service point exists and has matching minor --->
<cfquery name="qSP" datasource="payfrit">
  SELECT ID, BusinessID, BeaconMinor, Name
  FROM ServicePoints
  WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spId#">
  LIMIT 1
</cfquery>

<cfif qSP.recordCount EQ 0>
  <cfset apiAbort({ OK=false, ERROR="invalid_servicepoint", MESSAGE="Service point not found" })>
</cfif>

<cfif qSP.BusinessID NEQ bizId>
  <cfset apiAbort({ OK=false, ERROR="servicepoint_business_mismatch", MESSAGE="Service point does not belong to this business" })>
</cfif>

<!--- If service point doesn't have a minor yet, set it now --->
<cfif isNull(qSP.BeaconMinor) OR len(trim(qSP.BeaconMinor)) EQ 0>
  <cfquery datasource="payfrit">
    UPDATE ServicePoints
    SET BeaconMinor = <cfqueryparam cfsqltype="cf_sql_smallint" value="#minor#">
    WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spId#">
  </cfquery>
<cfelseif qSP.BeaconMinor NEQ minor>
  <cfset apiAbort({
    OK=false,
    ERROR="minor_mismatch",
    MESSAGE="Minor does not match service point's assigned value",
    ExpectedMinor=qSP.BeaconMinor,
    ProvidedMinor=minor
  })>
</cfif>

<!--- Check if hardware already exists --->
<cfquery name="qExisting" datasource="payfrit">
  SELECT ID, Status FROM BeaconHardware
  WHERE HardwareId = <cfqueryparam cfsqltype="cf_sql_varchar" value="#hardwareId#">
  LIMIT 1
</cfquery>

<cfif qExisting.recordCount GT 0>
  <!--- Update existing record --->
  <cfquery datasource="payfrit">
    UPDATE BeaconHardware
    SET
      BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">,
      ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spId#">,
      ShardUUID = <cfqueryparam cfsqltype="cf_sql_varchar" value="#beaconUUID#">,
      Major = <cfqueryparam cfsqltype="cf_sql_smallint" value="#major#">,
      Minor = <cfqueryparam cfsqltype="cf_sql_smallint" value="#minor#">,
      Status = 'assigned',
      <cfif len(txPower)>TxPower = <cfqueryparam cfsqltype="cf_sql_tinyint" value="#txPower#">,</cfif>
      <cfif len(advInterval)>AdvertisingInterval = <cfqueryparam cfsqltype="cf_sql_smallint" value="#advInterval#">,</cfif>
      <cfif len(firmwareVersion)>FirmwareVersion = <cfqueryparam cfsqltype="cf_sql_varchar" value="#firmwareVersion#">,</cfif>
      UpdatedAt = NOW()
    WHERE HardwareId = <cfqueryparam cfsqltype="cf_sql_varchar" value="#hardwareId#">
  </cfquery>
  <cfset hwId = qExisting.ID>
<cfelse>
  <!--- Insert new record --->
  <cfquery datasource="payfrit">
    INSERT INTO BeaconHardware (
      HardwareId,
      BusinessID,
      ServicePointID,
      ShardUUID,
      Major,
      Minor,
      Status
      <cfif len(txPower)>,TxPower</cfif>
      <cfif len(advInterval)>,AdvertisingInterval</cfif>
      <cfif len(firmwareVersion)>,FirmwareVersion</cfif>
    ) VALUES (
      <cfqueryparam cfsqltype="cf_sql_varchar" value="#hardwareId#">,
      <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">,
      <cfqueryparam cfsqltype="cf_sql_integer" value="#spId#">,
      <cfqueryparam cfsqltype="cf_sql_varchar" value="#beaconUUID#">,
      <cfqueryparam cfsqltype="cf_sql_smallint" value="#major#">,
      <cfqueryparam cfsqltype="cf_sql_smallint" value="#minor#">,
      'assigned'
      <cfif len(txPower)>,<cfqueryparam cfsqltype="cf_sql_tinyint" value="#txPower#"></cfif>
      <cfif len(advInterval)>,<cfqueryparam cfsqltype="cf_sql_smallint" value="#advInterval#"></cfif>
      <cfif len(firmwareVersion)>,<cfqueryparam cfsqltype="cf_sql_varchar" value="#firmwareVersion#"></cfif>
    )
  </cfquery>
  <cfquery name="qId" datasource="payfrit">
    SELECT LAST_INSERT_ID() AS ID
  </cfquery>
  <cfset hwId = qId.ID>
</cfif>

<cfoutput>#serializeJSON({
  OK = true,
  BeaconHardwareID = hwId,
  HardwareId = hardwareId,
  BusinessID = bizId,
  ServicePointID = spId,
  ServicePointName = qSP.Name,
  UUID = beaconUUID,
  Major = major,
  Minor = minor,
  Status = "assigned"
})#</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>
