<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" }
  Returns: { OK: true } always (don't reveal if email 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 {};
}

data = readJsonBody();
email = trim(data.Email ?: "");

if (!len(email)) {
  apiAbort({ "OK": false, "ERROR": "missing_email", "MESSAGE": "Email is required" });
}

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

try {
  // Look up user by 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" });

  // 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 email (skip on dev if magic OTP is enabled)
  if (!isDev) {
    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>
