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

<cfscript>
/*
  Send OTP Code for Portal Login
  POST: { "Email": "user@example.com" } or { "Phone": "3105551234" } or { "Identifier": "email or phone" }
  Returns: { OK: true } always (don't reveal if account exists)
*/

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) {
  // Strip all non-digits
  var digits = reReplace(phone, "[^0-9]", "", "all");
  // Remove leading 1 if 11 digits (US country code)
  if (len(digits) == 11 && left(digits, 1) == "1") {
    digits = right(digits, 10);
  }
  return digits;
}

function isPhoneNumber(input) {
  // Check if input looks like a phone number (mostly digits, possibly formatted)
  var digits = reReplace(input, "[^0-9]", "", "all");
  return len(digits) >= 10 && len(digits) <= 11;
}

data = readJsonBody();

// Accept Email, Phone, or generic Identifier field
identifier = trim(data.Identifier ?: data.Email ?: data.Phone ?: "");

if (!len(identifier)) {
  apiAbort({ "OK": false, "ERROR": "missing_identifier", "MESSAGE": "Email or phone is required" });
}

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

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

genericResponse = { "OK": true, "MESSAGE": "If an account exists, a code has been sent." };

try {
  // Look up user by email or phone
  if (len(email)) {
    qUser = queryTimed("
      SELECT ID, FirstName, ContactNumber
      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, ContactNumber
      FROM Users
      WHERE ContactNumber = :phone
        AND IsActive = 1
      LIMIT 1
    ", {
      phone: { value: phone, cfsqltype: "cf_sql_varchar" }
    }, { datasource: "payfrit" });
  }

  // Always return OK to not reveal if email exists
  if (qUser.recordCount == 0) {
    writeOutput(serializeJSON(genericResponse));
    abort;
  }

  userId = qUser.ID;

  // Rate limit: max 3 codes per user in last 10 minutes
  qRateCheck = queryTimed("
    SELECT COUNT(*) AS cnt
    FROM OTPCodes
    WHERE UserID = :userId
      AND CreatedAt > DATE_SUB(NOW(), INTERVAL 10 MINUTE)
  ", {
    userId: { value: userId, cfsqltype: "cf_sql_integer" }
  }, { datasource: "payfrit" });

  if (qRateCheck.cnt >= 3) {
    writeOutput(serializeJSON(genericResponse));
    abort;
  }

  // Generate 6-digit code (or use magic code on dev)
  code = randRange(100000, 999999);
  isDev = findNoCase("dev.payfrit.com", cgi.SERVER_NAME) > 0
       || findNoCase("localhost", cgi.SERVER_NAME) > 0;

  if (isDev && structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED
      && structKeyExists(application, "MAGIC_OTP_CODE") && len(application.MAGIC_OTP_CODE)) {
    code = application.MAGIC_OTP_CODE;
  }

  // Store in DB with 10-minute expiry
  queryTimed("
    INSERT INTO OTPCodes (UserID, Code, ExpiresAt)
    VALUES (:userId, :code, DATE_ADD(NOW(), INTERVAL 10 MINUTE))
  ", {
    userId: { value: userId, cfsqltype: "cf_sql_integer" },
    code: { value: code, cfsqltype: "cf_sql_varchar" }
  }, { datasource: "payfrit" });

  // Send OTP via email or SMS (skip on dev if magic OTP is enabled)
  if (!isDev) {
    if (len(phone) && len(qUser.ContactNumber)) {
      // Send via SMS
      try {
        if (structKeyExists(application, "twilioObj")) {
          smsMessage = "Your Payfrit login code is: " & code & ". It expires in 10 minutes.";
          application.twilioObj.sendSMS(to=qUser.ContactNumber, message=smsMessage);
        }
      } catch (any smsErr) {
        writeLog(file="otp_errors", text="SMS send failed for user #userId#: #smsErr.message#");
      }
    } else {
      // Send via email
      try {
        mailService = new mail();
        mailService.setTo(email);
        mailService.setFrom("admin@payfrit.com");
        mailService.setSubject("Your Payfrit login code");
        mailService.setType("html");

        emailBody = "
          <div style='font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 400px; margin: 0 auto; padding: 20px;'>
            <h2 style='color: ##333; margin-bottom: 8px;'>Your login code</h2>
            <p style='color: ##666; margin-bottom: 20px;'>Hi #encodeForHTML(qUser.FirstName)#, use this code to sign in to Payfrit:</p>
            <div style='background: ##f5f5f5; border-radius: 8px; padding: 20px; text-align: center; margin-bottom: 20px;'>
              <span style='font-size: 32px; font-weight: 700; letter-spacing: 6px; color: ##111;'>#code#</span>
            </div>
            <p style='color: ##999; font-size: 13px;'>This code expires in 10 minutes. If you didn't request this, you can safely ignore it.</p>
          </div>
        ";

        mailService.send(body=emailBody);
      } catch (any mailErr) {
        writeLog(file="otp_errors", text="Email send failed for user #userId#: #mailErr.message#");
      }
    }
  }

  // On dev, include the code in response for easy testing
  resp = duplicate(genericResponse);
  if (isDev) {
    resp["DEV_OTP"] = code;
  }
  writeOutput(serializeJSON(resp));

} catch (any e) {
  writeLog(file="otp_errors", text="sendLoginOTP error for #email#: #e.message#");
  writeOutput(serializeJSON(genericResponse));
}
</cfscript>
