<?php
// /form_send.php（公開から直接叩くエンドポイント）
// 役割：contact_form.html からの POST を受け、/data/contact.json に従ってメール送信（最小）
// 注意：CSRF・送信頻度制限は STEP9 で全体統合予定

declare(strict_types=1);

require_once __DIR__ . '/config.php';

// 補助：JSONリード（未定義ならフォールバック）
if (!function_exists('tpcms_read_json')) {
    function tpcms_read_json(string $path, $default = []) {
        if (is_file($path)) {
            $s = file_get_contents($path);
            $j = json_decode((string)$s, true);
            if (is_array($j) || is_object($j)) return $j;
        }
        return $default;
    }
}
function h(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function sanitize_header_value(string $s): string { return trim(preg_replace('/[\r\n]+/', ' ', $s)); }

// 設定読み込み
$contact = tpcms_read_json(__DIR__ . '/data/contact.json', []);
$to      = is_string($contact['to_email'] ?? null) ? trim($contact['to_email']) : '';
$subject = is_string($contact['subject']  ?? null) ? trim($contact['subject'])  : 'お問い合わせ';
$siteName  = is_string($contact['site_name']  ?? null) ? trim($contact['site_name'])  : '';
$autoReply = !empty($contact['auto_reply']);
$headerMsg = is_string($contact['header_msg'] ?? null) ? (string)$contact['header_msg'] : '';
$footerMsg = is_string($contact['footer_msg'] ?? null) ? (string)$contact['footer_msg'] : '';

// ラベル既定（contact.json の fields[] で上書き）
$labels = ['name'=>'お名前','email'=>'メール','message'=>'お問い合わせ内容'];
if (is_array($contact['fields'] ?? null)) {
    foreach ($contact['fields'] as $f) {
        if (!is_array($f)) continue;
        $k = $f['key'] ?? '';
        if (isset($labels[$k]) && is_string($f['label'] ?? null) && $f['label'] !== '') {
            $labels[$k] = $f['label'];
        }
    }
}

// POST 以外は 405
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
    http_response_code(405);
    header('Content-Type: text/html; charset=UTF-8');
    echo '<!doctype html><meta charset="utf-8"><title>405</title><p>このURLはフォームから送信してください。</p>';
    exit;
}

// --- CSRF 検証（公開フォーム）---
if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); }
$__csrf_post = isset($_POST['_csrf']) && is_scalar($_POST['_csrf']) ? (string)$_POST['_csrf'] : '';
$__csrf_sess = (string)($_SESSION['_csrf_form'] ?? '');

$__csrf_ok = ($__csrf_post !== '' && $__csrf_sess !== '' && hash_equals($__csrf_sess, $__csrf_post));
if (!$__csrf_ok) {
    // 不正：フォームへ戻す（?error=csrf）※CSRFは人が引っかかりにくいのでUIでの文言表示は後続ステップで必要なら対応
    $back = function_exists('tpcms_sanitize_back_url') ? tpcms_sanitize_back_url($_SERVER['HTTP_REFERER'] ?? '/') : ($_SERVER['HTTP_REFERER'] ?? '/');
    $sep = (strpos((string)$back, '?') === false) ? '?' : '&';
    header('Location: ' . $back . $sep . 'error=csrf', true, 303);
    exit;
}
// ワンタイム化（再送防止のため、通過後は無効化）
unset($_SESSION['_csrf_form']);

// --- Honeypot 検査（ボットは埋めがち）---
$__hp = isset($_POST['_hp_company']) ? trim((string)$_POST['_hp_company']) : '';
if ($__hp !== '') {
    $back = function_exists('tpcms_sanitize_back_url') ? tpcms_sanitize_back_url($_SERVER['HTTP_REFERER'] ?? '/') : ($_SERVER['HTTP_REFERER'] ?? '/');
    $sep = (strpos((string)$back, '?') === false) ? '?' : '&';
    header('Location: ' . $back . $sep . 'error=spam', true, 303);
    exit;
}

// --- Time Gate 検査（表示→送信までの最短秒数）---
$__tgMinSec = 3; // 初期値：3秒（必要なら後で5などへ調整）
$__tg = isset($_POST['_tg']) && is_scalar($_POST['_tg']) ? (int)$_POST['_tg'] : 0;
// トークンがあれば検査（古いフォーム互換のため、0はスキップ）
if ($__tg > 0) {
    if ((time() - $__tg) < $__tgMinSec) {
        $back = function_exists('tpcms_sanitize_back_url') ? tpcms_sanitize_back_url($_SERVER['HTTP_REFERER'] ?? '/') : ($_SERVER['HTTP_REFERER'] ?? '/');
        $sep = (strpos((string)$back, '?') === false) ? '?' : '&';
        header('Location: ' . $back . $sep . 'error=fast', true, 303);
        exit;
    }
}

// ---- ここから 送信レート制限（IP & プレフィックス） ----
$rateFile  = TPCMS_DATA . '/_rate_form.json';
$ip        = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$now       = time();
$windowSec = 60 * 60;

// 上限（必要に応じて調整可）
$limitIp   = 5;   // 既存：同一IP 1時間に5回
$limitPfx4 = 30;  // 追加：同一IPv4 /24 の合計 1時間に30回
$limitPfx6 = 60;  // 追加：同一IPv6 /64 の合計 1時間に60回

$rate = (is_file($rateFile) ? json_decode((string)@file_get_contents($rateFile), true) : []);
if (!is_array($rate)) $rate = [];

// 共通：古い記録を窓から落とす
$filterBucket = function ($arr) use ($now, $windowSec) {
    if (!is_array($arr)) return [];
    return array_values(array_filter($arr, function ($t) use ($now, $windowSec) {
        return is_int($t) && ($t > $now - $windowSec);
    }));
};

// --- IP単体バケット ---
$ipBucket = $filterBucket($rate[$ip] ?? null);

// --- プレフィックス特定 ---
$pfxKey = null;
$pfxLimit = null;

if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
    $parts = explode('.', $ip);
    if (count($parts) >= 3) {
        // 例: 203.0.113.x → pfx4:203.0.113
        $pfxKey   = 'pfx4:' . $parts[0] . '.' . $parts[1] . '.' . $parts[2];
        $pfxLimit = $limitPfx4;
    }
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
    $bin = @inet_pton($ip);
    if ($bin !== false && strlen($bin) === 16) {
        // 先頭64ビット（8バイト）をキー化（例: pfx6:xxxxxxxxxxxxxxxx）
        $pfxKey   = 'pfx6:' . bin2hex(substr($bin, 0, 8));
        $pfxLimit = $limitPfx6;
    }
}

// --- プレフィックスバケット ---
$pfxBucket = ($pfxKey !== null) ? $filterBucket($rate[$pfxKey] ?? null) : [];

// --- 上限判定（先に現在の残存数で判定。閾値到達時点で拒否） ---
if (count($ipBucket) >= $limitIp) {
    $back = function_exists('tpcms_sanitize_back_url')
        ? tpcms_sanitize_back_url($_SERVER['HTTP_REFERER'] ?? '/')
        : ($_SERVER['HTTP_REFERER'] ?? '/');
    $sep = (strpos((string)$back, '?') === false) ? '?' : '&';
    header('Location: ' . $back . $sep . 'error=rate_limit');
    http_response_code(303);
    exit;
}
if ($pfxKey !== null && count($pfxBucket) >= $pfxLimit) {
    $back = function_exists('tpcms_sanitize_back_url')
        ? tpcms_sanitize_back_url($_SERVER['HTTP_REFERER'] ?? '/')
        : ($_SERVER['HTTP_REFERER'] ?? '/');
    $sep = (strpos((string)$back, '?') === false) ? '?' : '&';
    header('Location: ' . $back . $sep . 'error=rate_limit');
    http_response_code(303);
    exit;
}

// --- 記録（成功/失敗問わずこの時点でカウント）---
$ipBucket[]  = $now;
$rate[$ip]   = $ipBucket;

if ($pfxKey !== null) {
    $pfxBucket[]    = $now;
    $rate[$pfxKey]  = $pfxBucket;
}

@file_put_contents($rateFile, json_encode($rate, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
@chmod($rateFile, 0660);
// ---- ここまで 送信レート制限 ----

// --- スキーマ抽出（fields[]）＋POST収集（次ステップで検証/本文組み立てに使用） ---
$fieldsDef = [];
if (is_array($contact['fields'] ?? null)) {
    foreach ($contact['fields'] as $f) {
        if (!is_array($f)) continue;
        $key = isset($f['key']) && is_string($f['key']) ? trim($f['key']) : '';
        if ($key === '' || !preg_match('/^[A-Za-z0-9_\-]+$/', $key)) continue;
        $type = isset($f['type']) && is_string($f['type']) ? strtolower($f['type']) : 'text';
        $label = isset($f['label']) && is_string($f['label']) ? trim($f['label']) : $key;
        $required = !empty($f['required']);
        $maxlength = (isset($f['maxlength']) && is_numeric($f['maxlength'])) ? (int)$f['maxlength'] : null;
        $options = (isset($f['options']) && is_array($f['options']))
            ? array_values(array_filter(array_map('strval', $f['options']), 'strlen'))
            : [];
        $fieldsDef[] = compact('key','type','label','required','maxlength','options');
    }
}
$inputs = [];
foreach ($fieldsDef as $d) {
    $k = $d['key'];
    $v = $_POST[$k] ?? '';
    if (is_array($v)) {
        // checkbox 等は配列で来る想定（未選択は空配列）
        $inputs[$k] = array_values(array_filter(array_map('strval', $v), 'strlen'));
    } elseif (is_scalar($v)) {
        $inputs[$k] = trim((string)$v);
    } else {
        $inputs[$k] = '';
    }
}
// --- /fields[] 下準備ここまで ---

// --- fields[] の軽量バリデーション＋本文断片の組み立て（※まだ送信に未適用） ---
$__tpcms_fields_errors = [];
$__tpcms_fields_clean  = [];
$__tpcms_fields_mail   = '';

if (!empty($fieldsDef)) {
    $mailItems = [];

    foreach ($fieldsDef as $d) {
        $k = $d['key'];
        $label = $d['label'];
        $type = $d['type'];
        $required  = (bool)$d['required'];
        $maxlength = $d['maxlength'];
        $options   = $d['options'];

        $raw = $inputs[$k] ?? '';

        // 値の正規化（配列 or 文字列）
        if (is_array($raw)) {
            $vals = array_values(array_filter(array_map('trim', $raw), 'strlen'));
        } else {
            $val  = trim((string)$raw);
            // checkbox 単一送信の保険（空なら空配列）
            $vals = ($type === 'checkbox') ? ($val === '' ? [] : [$val]) : [$val];
        }

        // 必須
        if ($required && count($vals) === 0) {
            $__tpcms_fields_errors[$k] = $label . 'は必須です。';
            $__tpcms_fields_clean[$k]  = $vals;
            $mailItems[] = $label . ': ';
            continue;
        }

        // options 検証（select/radio/checkbox）
        if (in_array($type, ['select','radio','checkbox'], true) && !empty($options)) {
            $allowed = array_flip($options);
            $vals = array_values(array_filter($vals, fn($v) => isset($allowed[$v])));
            if ($required && count($vals) === 0) {
                $__tpcms_fields_errors[$k] = $label . 'の選択が正しくありません。';
            }
        }

        // maxlength（text/textarea/email のみ）
        if ($maxlength && count($vals) === 1 && in_array($type, ['text','textarea','email'], true)) {
            if (mb_strlen($vals[0], 'UTF-8') > (int)$maxlength) {
                $__tpcms_fields_errors[$k] = $label . 'が長すぎます（最大' . (int)$maxlength . '文字）。';
            }
        }

        // email 形式
        if ($type === 'email' && count($vals) === 1 && $vals[0] !== '') {
            if (!filter_var($vals[0], FILTER_VALIDATE_EMAIL)) {
                $__tpcms_fields_errors[$k] = $label . 'の形式が正しくありません。';
            }
        }

        $__tpcms_fields_clean[$k] = $vals;
        $mailItems[] = $label . ': ' . (count($vals) ? implode(', ', $vals) : '');
    }

    $__tpcms_fields_mail = implode("\n", $mailItems);
}
// --- /fields[] 検証＋本文断片（まだ未適用） ---

// --- メール本文（fields[] + 既存3項目）組み立て（送信直前の置換用・このターンでは未使用） ---
$__tpcms_legacy_lines = [];
if (isset($name) && $name !== '')  $__tpcms_legacy_lines[] = 'お名前: ' . $name;
if (isset($email) && $email !== '') $__tpcms_legacy_lines[] = 'メール: ' . $email;
if (isset($message) && $message !== '') {
    $__tpcms_legacy_lines[] = '本文:';
    $__tpcms_legacy_lines[] = $message;
}

$__tpcms_mail_body_admin = implode("\n\n", array_filter([
    ($__tpcms_fields_mail !== '' ? "■入力内容\n" . $__tpcms_fields_mail : null),
    (count($__tpcms_legacy_lines) ? "■従来フィールド\n" . implode("\n", $__tpcms_legacy_lines) : null),
]));

$__tpcms_mail_body_user = $__tpcms_mail_body_admin; // ひとまず同一内容（必要なら次ターン以降で別文面にできます）
// --- /メール本文 組み立てここまで ---

// --- fields[] メール本文の最終整形（ヘッダー/フッター適用・CRLF 正規化）---
// ヘッダー/フッター文字列の取得（既存変数 or contact.json の複数候補）
$__tpcms_header = '';
$__tpcms_footer = '';
foreach (['headerMsg','header_text','header','mail_header'] as $__k) {
    if (isset($$__k) && is_string($$__k) && $$__k !== '') { $__tpcms_header = $$__k; break; }
    if (isset($contact[$__k]) && is_string($contact[$__k]) && $contact[$__k] !== '') { $__tpcms_header = $contact[$__k]; break; }
}
foreach (['footerMsg','footer_text','footer','mail_footer'] as $__k) {
    if (isset($$__k) && is_string($$__k) && $$__k !== '') { $__tpcms_footer = $$__k; break; }
    if (isset($contact[$__k]) && is_string($contact[$__k]) && $contact[$__k] !== '') { $__tpcms_footer = $contact[$__k]; break; }
}

$__tpcms_final_admin_body = trim(implode("\n\n", array_filter([
    $__tpcms_header !== '' ? $__tpcms_header : null,
    $__tpcms_mail_body_admin !== '' ? $__tpcms_mail_body_admin : null,
    $__tpcms_footer !== '' ? $__tpcms_footer : null,
])));
$__tpcms_final_user_body = trim(implode("\n\n", array_filter([
    $__tpcms_header !== '' ? $__tpcms_header : null,
    $__tpcms_mail_body_user !== '' ? $__tpcms_mail_body_user : null,
    $__tpcms_footer !== '' ? $__tpcms_footer : null,
])));

// 改行をメール向け CRLF に
$__tpcms_final_admin_body = preg_replace("/\r\n|\r|\n/u", "\r\n", $__tpcms_final_admin_body);
$__tpcms_final_user_body  = preg_replace("/\r\n|\r|\n/u", "\r\n", $__tpcms_final_user_body);
// --- /最終整形 ここまで ---

// 入力
$name    = isset($_POST['name'])    && is_scalar($_POST['name'])    ? trim((string)$_POST['name'])    : '';
$email   = isset($_POST['email'])   && is_scalar($_POST['email'])   ? trim((string)$_POST['email'])   : '';
$message = isset($_POST['message']) && is_scalar($_POST['message']) ? trim((string)$_POST['message']) : '';

// 検証（最小）
$errors = [];
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) $errors[] = '送信先メールアドレスが正しく設定されていません。';
if ($name === '')  $errors[] = $labels['name'] . 'は必須です。';
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = $labels['email'] . 'が不正です。';
if ($message === '') $errors[] = $labels['message'] . 'は必須です。';

if ($errors) {
    header('Content-Type: text/html; charset=UTF-8');
    echo '<!doctype html><meta charset="utf-8"><title>送信エラー</title>';
    echo '<h1>送信できませんでした</h1><ul>';
    foreach ($errors as $e) echo '<li>' . h($e) . '</li>';
    echo '</ul><p><a href="javascript:history.back()">戻る</a></p>';
    exit;
}

/* メール送信 */

// 件名（一般的な日本の運用：件名に to_email は入れない）
$prefix = ($siteName !== '') ? '【' . $siteName . '】' : '';
$subjectAdmin = $prefix . $subject;                 // 管理者宛：設定 subject を尊重
$subjectUser  = $prefix . 'お問い合わせありがとうございます'; // 自動返信：定型

// 共通ヘッダ（JIS & 7bit）。From は表示名付きで送信
$headersBase  = "MIME-Version: 1.0\r\n";
$headersBase .= "Content-Type: text/plain; charset=ISO-2022-JP\r\n";
$headersBase .= "Content-Transfer-Encoding: 7bit\r\n";

$fromAddr = sanitize_header_value($to);
if ($fromAddr !== '') {
    // サイト名を表示名として MIME エンコードして付与（一般的）
    if ($siteName !== '') {
        $fromName = mb_encode_mimeheader($siteName, 'ISO-2022-JP-MS');
        $headersBase .= "From: {$fromName} <{$fromAddr}>\r\n";
    } else {
        $headersBase .= "From: {$fromAddr}\r\n";
    }
}

// ヘッダ（管理者宛）：返信先はユーザー
$headersAdmin = $headersBase;
if ($email !== '') $headersAdmin .= "Reply-To: " . sanitize_header_value($email) . "\r\n";

// 送信（管理者宛）
$to = trim((string)($contact['to_email'] ?? $to));
$sentAdmin = @mb_send_mail($to, $subjectAdmin, mb_convert_encoding($__tpcms_final_admin_body, 'ISO-2022-JP-MS', 'UTF-8'), $headersAdmin);

// 自動返信（ユーザー宛）：有効時のみ。IP/日時は入れない
if ($autoReply && filter_var($email, FILTER_VALIDATE_EMAIL)) {

    // ユーザー宛：返信先は管理側（＝サイトのアドレス）
    $headersUser = $headersBase;
    if ($fromAddr !== '') $headersUser .= "Reply-To: {$fromAddr}\r\n";

    $sentUser = @mb_send_mail($email, $subjectUser, mb_convert_encoding($__tpcms_final_user_body, 'ISO-2022-JP-MS', 'UTF-8'), $headersUser);
}

// 以降のPRGで参照する成否は、管理者宛の送信結果を採用
$sent = $sentAdmin;

/* 応答（PRG：送信後はテンプレ適用ページへ戻す） */
function tpcms_sanitize_back_url(?string $ref): string {
    if (!is_string($ref) || $ref === '') return '/';
    $u = parse_url($ref);
    if ($u === false) return '/';
    // 同一ホストのみ許可（外部リダイレクト防止）
    $host = $_SERVER['HTTP_HOST'] ?? '';
    if (isset($u['host']) && strcasecmp($u['host'], $host) !== 0) return '/';
    $path = $u['path'] ?? '/';
    $query = isset($u['query']) ? ('?' . $u['query']) : '';
    $fragment = isset($u['fragment']) ? ('#' . $u['fragment']) : '';
    return $path . $query . $fragment;
}

$back = tpcms_sanitize_back_url($_SERVER['HTTP_REFERER'] ?? '/');

if ($sent) {
    // 成功時：?sent=1 を付けて 303 リダイレクト（テンプレ適用ページで表示）
    $sep = (strpos($back, '?') === false) ? '?' : '&';
    header('Location: ' . $back . $sep . 'sent=1', true, 303);
} else {
    // 失敗時：元ページへ戻す（必要なら今後 ?error=1 などに拡張）
    header('Location: ' . $back, true, 303);
}
exit;
