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

<cfscript>
/*
  Verify OTP Code for Portal Login (supports email or phone)
  POST: { "Email": "user@example.com", "Code": "123456" }
     or { "Phone": "3105551234", "Code": "123456" }
     or { "Identifier": "email or phone", "Code": "123456" }
  Returns: { OK: true, UserID, FirstName, Token } or { OK: false, ERROR: "invalid_code" }
*/

function apiAbort(required struct payload) {
  writeOutput(serializeJSON(payload));
  abort;
}

function readJsonBody() {
  var raw = getHttpRequestData().content;
  if (isNull(raw)) raw = "";
  if (!len(trim(raw))) return {};
  try {
    var data = deserializeJSON(raw);
    if (isStruct(data)) return data;
  } catch (any e) {}
  return {};
}

function normalizePhone(phone) {
  var digits = reReplace(phone, "[^0-9]", "", "all");
  if (len(digits) == 11 && left(digits, 1) == "1") {
    digits = right(digits, 10);
  }
  return digits;
}

function isPhoneNumber(input) {
  var digits = reReplace(input, "[^0-9]", "", "all");
  return len(digits) >= 10 && len(digits) <= 11;
}

data = readJsonBody();
identifier = trim(data.Identifier ?: data.Email ?: data.Phone ?: "");
code = trim(data.Code ?: "");

if (!len(identifier) || !len(code)) {
  apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "Email/phone and code are required" });
}

// Determine if this is email or phone
isPhone = isPhoneNumber(identifier);
email = "";
phone = "";

if (isPhone) {
  phone = normalizePhone(identifier);
} else {
  email = identifier;
}

try {
  // Look up user by email or phone
  if (len(email)) {
    qUser = queryTimed("
      SELECT ID, FirstName
      FROM Users
      WHERE EmailAddress = :email
        AND IsActive = 1
      LIMIT 1
    ", {
      email: { value: email, cfsqltype: "cf_sql_varchar" }
    }, { datasource: "payfrit" });
  } else {
    qUser = queryTimed("
      SELECT ID, FirstName
      FROM Users
      WHERE ContactNumber = :phone
        AND IsActive = 1
      LIMIT 1
    ", {
      phone: { value: phone, cfsqltype: "cf_sql_varchar" }
    }, { datasource: "payfrit" });
  }

  if (qUser.recordCount == 0) {
    apiAbort({ "OK": false, "ERROR": "invalid_code", "MESSAGE": "Invalid or expired code" });
  }

  userId = qUser.ID;

  // Check for valid OTP in OTPCodes table
  qOTP = queryTimed("
    SELECT ID
    FROM OTPCodes
    WHERE UserID = :userId
      AND Code = :code
      AND ExpiresAt > NOW()
      AND UsedAt IS NULL
    ORDER BY CreatedAt DESC
    LIMIT 1
  ", {
    userId: { value: userId, cfsqltype: "cf_sql_integer" },
    code: { value: code, cfsqltype: "cf_sql_varchar" }
  }, { datasource: "payfrit" });

  if (qOTP.recordCount == 0) {
    apiAbort({ "OK": false, "ERROR": "invalid_code", "MESSAGE": "Invalid or expired code" });
  }

  // Mark OTP as used
  queryTimed("
    UPDATE OTPCodes
    SET UsedAt = NOW()
    WHERE ID = :otpId
  ", {
    otpId: { value: qOTP.ID, cfsqltype: "cf_sql_integer" }
  }, { datasource: "payfrit" });

  // Create auth token (same as login.cfm)
  token = replace(createUUID(), "-", "", "all");

  queryTimed(
    "INSERT INTO UserTokens (UserID, Token) VALUES (?, ?)",
    [
      { value: userId, cfsqltype: "cf_sql_integer" },
      { value: token, cfsqltype: "cf_sql_varchar" }
    ],
    { datasource: "payfrit" }
  );

  // Set session
  lock timeout="15" throwontimeout="yes" type="exclusive" scope="session" {
    session.UserID = userId;
  }
  request.UserID = userId;

  writeOutput(serializeJSON({
    "OK": true,
    "ERROR": "",
    "UserID": userId,
    "FirstName": qUser.FirstName,
    "Token": token
  }));

} catch (any e) {
  apiAbort({
    "OK": false,
    "ERROR": "server_error",
    "MESSAGE": e.message
  });
}
</cfscript>
