<?php
/**
 * AigentZone website contact endpoint.
 *
 * Browser → POST /contact-handler.php (JSON) → PHPMailer → authenticated SMTP
 * → aalfarra@aigentzone.ae
 *
 * This file lives in Next.js `public/`, so it is copied verbatim to the site
 * root on every build and keeps working after the planned static export. There
 * is deliberately no Next.js API route, server action or Node process involved.
 *
 * IT CONTAINS NO SECRETS. The SMTP password is read at runtime from either the
 * AIGENTZONE_SMTP_PASSWORD environment variable or a config file kept OUTSIDE
 * public_html (see server/README.md). Next's dev server serves files in
 * public/ as static assets rather than executing them, so this source may be
 * readable in development — which is safe precisely because nothing secret is
 * in it.
 *
 * Targets PHP 7.4+ (cPanel default here is ea-php81).
 */

declare(strict_types=1);

// ── Runtime posture ────────────────────────────────────────────────────────
// Never render an error to the response: the client must only ever see the
// JSON contract below, and a warning printed before the body would corrupt it.
@ini_set('display_errors', '0');
@ini_set('display_startup_errors', '0');
@ini_set('html_errors', '0');
error_reporting(E_ALL);
date_default_timezone_set('Asia/Dubai');

// ── Configuration that is safe to publish ──────────────────────────────────
const SMTP_HOST      = 'mail.aigentzone.ae';
const SMTP_PORT      = 465;                       // SMTPS (implicit TLS)
const SMTP_USERNAME  = 'web@aigentzone.ae';
const FROM_EMAIL     = 'web@aigentzone.ae';
const FROM_NAME      = 'AigentZone Website';
const RECIPIENT      = 'aalfarra@aigentzone.ae';

/** Hosts allowed to originate a submission. */
const ALLOWED_HOSTS = ['aigentzone.ae', 'www.aigentzone.ae', 'localhost', '127.0.0.1'];

/** Maximum accepted length per field, in UTF-8 characters. */
const LIMITS = [
    'fullName' => 150,
    'email'    => 254,
    'phone'    => 50,
    'company'  => 200,
    'message'  => 5000,
    'page'     => 500,
];

/** Largest request body accepted at all, before parsing. */
const MAX_BODY_BYTES = 65536;

/**
 * The inquiry chips offered by the form. Only these keys are accepted, and the
 * English label is what reaches the subject line — visitor input never does.
 */
const INQUIRY_LABELS = [
    'demo'        => 'Book a Demo',
    'omnilink'    => 'OmniLink',
    'aigentpos'   => 'AigentPOS',
    'partnership' => 'Partnership',
    'general'     => 'General Inquiry',
];

/** A human cannot complete this form faster than this. */
const MIN_FILL_MS = 2500;

/** Rate limit: submissions allowed per IP within the rolling window. */
const RATE_LIMIT_MAX    = 6;
const RATE_LIMIT_WINDOW = 3600; // seconds

// ── Response contract ──────────────────────────────────────────────────────

/**
 * Emit the JSON response and stop. The client is told what happened in broad
 * terms and nothing else — no exception text, no SMTP transcript, no paths.
 */
function respond(int $status, array $payload): void
{
    if (!headers_sent()) {
        http_response_code($status);
        header('Content-Type: application/json; charset=utf-8');
        header('X-Content-Type-Options: nosniff');
        header('Cache-Control: no-store, max-age=0');
        header('Referrer-Policy: same-origin');
    }
    echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function fail(string $error, int $status = 400): void
{
    respond($status, ['success' => false, 'error' => $error]);
}

function succeed(): void
{
    respond(200, ['success' => true]);
}

/** Server-side only. Detail stays here; the visitor sees a generic failure. */
function log_detail(string $message, array $config = []): void
{
    $line = '[aigentzone-contact] ' . $message;
    if (!empty($config['log_file']) && is_string($config['log_file'])) {
        @error_log(date('c') . ' ' . $line . PHP_EOL, 3, $config['log_file']);
        return;
    }
    @error_log($line);
}

// ── Private configuration (never in public_html, never in git) ─────────────

/**
 * Merge the deployment's private settings over the published defaults.
 *
 * Order of preference:
 *   1. AIGENTZONE_SMTP_PASSWORD environment variable
 *   2. a PHP file outside public_html returning an array
 *
 * A missing config file is not an error as long as the environment supplies
 * the password.
 */
function load_config(): array
{
    $config = [
        'smtp_host'      => SMTP_HOST,
        'smtp_port'      => SMTP_PORT,
        'smtp_username'  => SMTP_USERNAME,
        'smtp_password'  => '',
        'from_email'     => FROM_EMAIL,
        'from_name'      => FROM_NAME,
        'recipient'      => RECIPIENT,
        'rate_limit_dir' => '',
        'log_file'       => '',
        'ip_salt'        => '',
        'smtp_debug'     => 0,
    ];

    $candidates = [];
    $fromEnv = getenv('AIGENTZONE_CONTACT_CONFIG');
    if (is_string($fromEnv) && $fromEnv !== '') {
        $candidates[] = $fromEnv;
    }
    $candidates[] = __DIR__ . '/../private/aigentzone-contact-config.php';
    $candidates[] = dirname(__DIR__, 2) . '/private/aigentzone-contact-config.php';

    foreach ($candidates as $path) {
        if (is_string($path) && $path !== '' && is_readable($path)) {
            /** @noinspection PhpIncludeInspection — fixed paths only, never user input. */
            $loaded = require $path;
            if (is_array($loaded)) {
                $config = array_merge($config, $loaded);
            }
            break;
        }
    }

    // The environment variable wins, so a host that supports env vars never
    // needs the file at all.
    $envPassword = getenv('AIGENTZONE_SMTP_PASSWORD');
    if (is_string($envPassword) && $envPassword !== '') {
        $config['smtp_password'] = $envPassword;
    }

    return $config;
}

/** Locate the Composer autoloader, preferring an install outside public_html. */
function load_phpmailer(): bool
{
    $candidates = [];
    $fromEnv = getenv('AIGENTZONE_VENDOR_AUTOLOAD');
    if (is_string($fromEnv) && $fromEnv !== '') {
        $candidates[] = $fromEnv;
    }
    $candidates[] = __DIR__ . '/../private/vendor/autoload.php';
    $candidates[] = dirname(__DIR__, 2) . '/private/vendor/autoload.php';
    $candidates[] = __DIR__ . '/../vendor/autoload.php';
    $candidates[] = __DIR__ . '/vendor/autoload.php';

    foreach ($candidates as $path) {
        if (is_string($path) && $path !== '' && is_readable($path)) {
            /** @noinspection PhpIncludeInspection — fixed paths only, never user input. */
            require_once $path;
            break;
        }
    }

    return class_exists('PHPMailer\\PHPMailer\\PHPMailer');
}

// ── Input helpers ──────────────────────────────────────────────────────────

/** Control characters have no place in a form field and are a header-injection tell. */
function has_control_chars(string $value, bool $allowNewlines = false): bool
{
    $pattern = $allowNewlines
        ? '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u'
        : '/[\x00-\x1F\x7F]/u';

    return (bool) preg_match($pattern, $value);
}

/**
 * Pull one string field: type-checked, newline-normalised, trimmed, and
 * length-capped. Anything over the limit is rejected rather than silently
 * truncated, so the visitor is told instead of losing the end of their message.
 */
function read_field(array $data, string $key, int $max, bool $allowNewlines = false): string
{
    $value = isset($data[$key]) && is_string($data[$key]) ? $data[$key] : '';

    if ($value !== '' && !mb_check_encoding($value, 'UTF-8')) {
        fail('validation_error');
    }

    $value = str_replace(["\r\n", "\r"], "\n", $value);
    if (!$allowNewlines) {
        $value = str_replace("\n", ' ', $value);
    }
    $value = trim($value);

    if (has_control_chars($value, $allowNewlines)) {
        fail('validation_error');
    }
    if (mb_strlen($value, 'UTF-8') > $max) {
        fail('validation_error');
    }

    return $value;
}

function esc(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

/** Host of a URL, lowercased, or '' when it cannot be parsed. */
function url_host(string $url): string
{
    $host = parse_url($url, PHP_URL_HOST);

    return is_string($host) ? strtolower($host) : '';
}

// ── Lightweight per-IP rate limiting ───────────────────────────────────────

/**
 * A rolling window kept in one small file per hashed IP.
 *
 * The raw address is never written down: it is HMAC'd into an opaque key and
 * only timestamps are stored. If the directory is not writable the limiter
 * stands down rather than blocking real customers.
 */
function rate_limit_exceeded(array $config): bool
{
    $ip = isset($_SERVER['REMOTE_ADDR']) && is_string($_SERVER['REMOTE_ADDR'])
        ? $_SERVER['REMOTE_ADDR']
        : '';
    if ($ip === '') {
        return false;
    }

    $dir = is_string($config['rate_limit_dir']) && $config['rate_limit_dir'] !== ''
        ? $config['rate_limit_dir']
        : sys_get_temp_dir() . '/aigentzone-contact-rate';

    if (!is_dir($dir) && !@mkdir($dir, 0700, true) && !is_dir($dir)) {
        log_detail('rate limit directory unavailable, limiter disabled', $config);
        return false;
    }

    $salt = is_string($config['ip_salt']) && $config['ip_salt'] !== ''
        ? $config['ip_salt']
        : 'aigentzone-contact';
    $key = substr(hash_hmac('sha256', $ip, $salt), 0, 32);
    $file = $dir . '/' . $key;

    $now = time();
    $handle = @fopen($file, 'c+');
    if ($handle === false) {
        return false;
    }

    $exceeded = false;
    if (@flock($handle, LOCK_EX)) {
        $contents = (string) stream_get_contents($handle);
        $stamps = array_filter(
            array_map('intval', preg_split('/\s+/', trim($contents)) ?: []),
            static function (int $t) use ($now): bool {
                return $t > $now - RATE_LIMIT_WINDOW;
            }
        );

        if (count($stamps) >= RATE_LIMIT_MAX) {
            $exceeded = true;
        } else {
            $stamps[] = $now;
        }

        ftruncate($handle, 0);
        rewind($handle);
        fwrite($handle, implode("\n", $stamps));
        fflush($handle);
        flock($handle, LOCK_UN);
    }
    fclose($handle);

    // Occasional sweep so the directory cannot grow without bound.
    if (random_int(1, 50) === 1) {
        foreach ((array) @glob($dir . '/*') as $old) {
            if (is_string($old) && @filemtime($old) < $now - RATE_LIMIT_WINDOW * 2) {
                @unlink($old);
            }
        }
    }

    return $exceeded;
}

// ── Request gate ───────────────────────────────────────────────────────────

$method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper((string) $_SERVER['REQUEST_METHOD']) : '';
if ($method !== 'POST') {
    header('Allow: POST');
    fail('method_not_allowed', 405);
}

/*
 * JSON only, on purpose. A cross-origin HTML form can only ever send
 * form-encoded, multipart or text/plain bodies without tripping a CORS
 * preflight — and no CORS headers are sent here — so requiring
 * application/json rules out drive-by cross-site posts on its own. No
 * Access-Control-Allow-Origin is set anywhere in this file.
 */
$contentType = isset($_SERVER['CONTENT_TYPE']) ? strtolower((string) $_SERVER['CONTENT_TYPE']) : '';
if (strpos($contentType, 'application/json') === false) {
    fail('unsupported_media_type', 415);
}

// When the browser sends an Origin, it has to be one of ours. Server-side
// tooling (curl, cron checks) sends none, and is allowed through so the
// endpoint stays testable on the server.
$origin = isset($_SERVER['HTTP_ORIGIN']) ? (string) $_SERVER['HTTP_ORIGIN'] : '';
if ($origin !== '') {
    $originHost = url_host($origin);
    $requestHost = isset($_SERVER['HTTP_HOST'])
        ? strtolower((string) preg_replace('/:\d+$/', '', (string) $_SERVER['HTTP_HOST']))
        : '';
    $allowed = array_merge(ALLOWED_HOSTS, $requestHost !== '' ? [$requestHost] : []);
    if ($originHost === '' || !in_array($originHost, $allowed, true)) {
        fail('forbidden_origin', 403);
    }
}

$raw = file_get_contents('php://input');
if ($raw === false || $raw === '' || strlen($raw) > MAX_BODY_BYTES) {
    fail('validation_error');
}

$data = json_decode($raw, true);
if (!is_array($data)) {
    fail('validation_error');
}

$config = load_config();

// ── Silent spam traps ──────────────────────────────────────────────────────
// Both report success. A bot learns nothing about why its submission vanished,
// and neither trap can misfire on a visitor who is simply typing quickly,
// because the timing check below reports a normal failure they can retry.

$honeypot = isset($data['website']) && is_string($data['website']) ? trim($data['website']) : '';
if ($honeypot !== '') {
    log_detail('honeypot triggered, submission discarded', $config);
    succeed();
}

// ── Timing ─────────────────────────────────────────────────────────────────
// Measured in the browser, so it is advisory rather than a guarantee — it is
// one signal among several, never the only gate. Reported as an ordinary
// validation failure so a fast (or clock-skewed) human can simply resubmit.
$elapsedMs = isset($data['elapsedMs']) && is_numeric($data['elapsedMs']) ? (int) $data['elapsedMs'] : 0;
if ($elapsedMs > 0 && $elapsedMs < MIN_FILL_MS) {
    log_detail('submission faster than the minimum fill time', $config);
    fail('validation_error');
}

// ── Field validation ───────────────────────────────────────────────────────

$fullName = read_field($data, 'fullName', LIMITS['fullName']);
$email    = read_field($data, 'email', LIMITS['email']);
$phone    = read_field($data, 'phone', LIMITS['phone']);
$company  = read_field($data, 'company', LIMITS['company']);
$message  = read_field($data, 'message', LIMITS['message'], true);
$page     = read_field($data, 'page', LIMITS['page']);

if ($fullName === '' || $email === '' || $message === '') {
    fail('validation_error');
}

// filter_var also rejects the CR/LF that would let a header be forged; the
// control-character check above has already removed that possibility.
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    fail('validation_error');
}

$inquiryKey = isset($data['inquiry']) && is_string($data['inquiry']) ? $data['inquiry'] : '';
$inquiryLabel = array_key_exists($inquiryKey, INQUIRY_LABELS) ? INQUIRY_LABELS[$inquiryKey] : '';

$locale = (isset($data['locale']) && $data['locale'] === 'ar') ? 'ar' : 'en';
$localeLabel = $locale === 'ar' ? 'Arabic' : 'English';

// Only keep a page URL that belongs to this site; anything else is reduced to
// its path so the email can never carry an attacker-chosen link.
if ($page !== '') {
    $pageHost = url_host($page);
    if ($pageHost !== '' && !in_array($pageHost, ALLOWED_HOSTS, true)) {
        $path = parse_url($page, PHP_URL_PATH);
        $page = is_string($path) ? $path : '';
    }
}

if (rate_limit_exceeded($config)) {
    log_detail('rate limit reached for requester', $config);
    fail('rate_limited', 429);
}

// ── Compose ────────────────────────────────────────────────────────────────

$submittedAt = date('Y-m-d H:i') . ' (GST)';

$rows = [
    ['Name', $fullName],
    ['Email', $email],
    ['Phone', $phone],
    ['Company', $company],
    ['Interested In', $inquiryLabel],
    ['Message', $message],
    ['Page', $page],
    ['Language', $localeLabel],
    ['Submitted', $submittedAt],
];

$htmlRows = '';
$textLines = ['AigentZone Website Lead', str_repeat('=', 24), ''];

foreach ($rows as [$label, $value]) {
    if ($value === '') {
        continue; // only submitted fields appear
    }

    $isMessage = $label === 'Message';
    $htmlValue = esc($value);
    if ($isMessage) {
        $htmlValue = nl2br($htmlValue, false);
    }
    $dirAttr = ($isMessage && $locale === 'ar') ? ' dir="rtl"' : '';

    $htmlRows .= '<tr>'
        . '<td style="padding:10px 0 2px;font:600 11px/1.4 Arial,Helvetica,sans-serif;'
        . 'letter-spacing:.08em;text-transform:uppercase;color:#6D6F71;">' . esc($label) . '</td>'
        . '</tr><tr>'
        . '<td' . $dirAttr . ' style="padding:0 0 10px;border-bottom:1px solid #ECE9E3;'
        . 'font:400 15px/1.6 Arial,Helvetica,sans-serif;color:#0f172a;word-break:break-word;">'
        . $htmlValue . '</td>'
        . '</tr>';

    $textLines[] = $label . ':';
    $textLines[] = $value;
    $textLines[] = '';
}

$html = '<!doctype html><html><head><meta charset="utf-8">'
    . '<meta name="viewport" content="width=device-width,initial-scale=1"></head>'
    . '<body style="margin:0;padding:24px;background:#faf9f6;">'
    . '<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" '
    . 'style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:14px;'
    . 'border:1px solid #ECE9E3;">'
    . '<tr><td style="padding:24px 28px;background:#0b1324;border-radius:14px 14px 0 0;">'
    . '<div style="font:800 18px/1.2 Arial,Helvetica,sans-serif;color:#ffffff;">'
    . 'Aigent<span style="color:#FFCE75;">Zone</span></div>'
    . '<div style="margin-top:6px;font:600 11px/1.4 Arial,Helvetica,sans-serif;'
    . 'letter-spacing:.14em;text-transform:uppercase;color:rgba(255,255,255,.45);">'
    . 'Website Lead</div>'
    . '</td></tr>'
    . '<tr><td style="padding:8px 28px 24px;">'
    . '<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">'
    . $htmlRows
    . '</table></td></tr></table></body></html>';

$text = implode("\n", $textLines);

$subject = 'New Website Lead — ';
if ($inquiryLabel !== '') {
    $subject .= $inquiryLabel . ' — ';
}
$subject .= 'AigentZone';

// ── Send ───────────────────────────────────────────────────────────────────

if (!load_phpmailer()) {
    log_detail('PHPMailer autoloader not found — run composer install (see server/README.md)', $config);
    fail('send_failed', 500);
}

$password = is_string($config['smtp_password']) ? $config['smtp_password'] : '';
if ($password === '') {
    log_detail('SMTP password not configured — set AIGENTZONE_SMTP_PASSWORD or the private config file', $config);
    fail('send_failed', 500);
}

$mail = new PHPMailer\PHPMailer\PHPMailer(true);

try {
    $mail->isSMTP();
    $mail->Host       = (string) $config['smtp_host'];
    $mail->Port       = (int) $config['smtp_port'];
    $mail->SMTPAuth   = true;
    $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS; // implicit TLS on 465
    $mail->SMTPAutoTLS = false;                                          // already encrypted
    $mail->Username   = (string) $config['smtp_username'];
    $mail->Password   = $password;
    $mail->CharSet    = 'UTF-8';
    $mail->Encoding   = 'base64';                                        // safe for Arabic
    $mail->Timeout    = 20;

    // Debug output must never reach the response body. When switched on in the
    // private config it goes to the server-side log and nowhere else.
    $debugLevel = (int) $config['smtp_debug'];
    if ($debugLevel > 0) {
        $mail->SMTPDebug = $debugLevel;
        $mail->Debugoutput = static function (string $line) use ($config): void {
            log_detail('smtp: ' . trim($line), $config);
        };
    }

    // Envelope sender is always our own authenticated mailbox, so SPF and
    // DMARC align. The visitor is reachable through Reply-To instead.
    $mail->setFrom((string) $config['from_email'], (string) $config['from_name']);
    $mail->addAddress((string) $config['recipient']);
    $mail->addReplyTo($email, $fullName !== '' ? $fullName : $email);

    $mail->Subject = $subject;
    $mail->isHTML(true);
    $mail->Body    = $html;
    $mail->AltBody = $text;

    // No silent fallback to mail(): if SMTP will not deliver, the caller is told.
    $mail->send();
} catch (Throwable $e) {
    log_detail('send failed: ' . $e->getMessage(), $config);
    fail('send_failed', 502);
}

succeed();
