<?php
// Phonebook API — JSON-backed, admin approvals, photo uploads.

declare(strict_types=1);
ini_set('display_errors', '0');
error_reporting(E_ALL);

define('ROOT', __DIR__);
define('ADMIN_COOKIE', 'pbAdmin');

header('Content-Type: application/json; charset=utf-8');

// ---------- helpers ----------
function ok(array $data = []): void { echo json_encode(array_merge(['ok' => true], $data), JSON_UNESCAPED_UNICODE); exit; }
function fail(int $http, string $msg): void {
    http_response_code($http);
    echo json_encode(['ok' => false, 'error' => $msg], JSON_UNESCAPED_UNICODE);
    exit;
}
function readJsonFile(string $path, $default) {
    if (!file_exists($path)) return $default;
    $raw = @file_get_contents($path);
    if ($raw === false || $raw === '') return $default;
    $j = json_decode($raw, true);
    return is_array($j) ? $j : $default;
}
function writeJsonFile(string $path, $data): void {
    $dir = dirname($path);
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    $tmp = $path . '.tmp';
    file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
    @rename($tmp, $path);
}
function withLock(string $lockFile, callable $fn) {
    $fp = @fopen($lockFile, 'c+');
    if (!$fp) return $fn();
    @flock($fp, LOCK_EX);
    try { return $fn(); } finally { @flock($fp, LOCK_UN); @fclose($fp); }
}
function withLockedContacts(callable $fn) {
    $lock = ROOT . '/data/.contacts.lock';
    if (!is_dir(dirname($lock))) @mkdir(dirname($lock), 0775, true);
    return withLock($lock, function () use ($fn) {
        $path = ROOT . '/data/contacts.json';
        $data = readJsonFile($path, ['contacts' => []]);
        $new = $fn($data);
        if ($new !== null) writeJsonFile($path, $new);
        return $new ?? $data;
    });
}
function input(string $key, $default = null) { return $_POST[$key] ?? $_GET[$key] ?? $default; }
function jsonInput(): array {
    $raw = file_get_contents('php://input');
    if (!$raw) return [];
    $j = json_decode($raw, true);
    return is_array($j) ? $j : [];
}
function trimStr(string $s, int $max = 500): string {
    $s = preg_replace('/\s+/u', ' ', trim($s));
    return mb_substr($s, 0, $max, 'UTF-8');
}
function clientIp(): string {
    $ip = $_SERVER['REMOTE_ADDR'] ?? '';
    if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ip = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
    }
    return $ip ?: '0.0.0.0';
}
function newId(): string {
    return substr(bin2hex(random_bytes(6)), 0, 10);
}
function newToken(): string { return bin2hex(random_bytes(24)); }

function requireMethod(string $m): void {
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== $m) fail(405, 'method not allowed');
}

// ---------- rate limit ----------
function rateLimit(string $bucket, int $max, int $window = 3600): void {
    $path = ROOT . '/data/ratelimit.json';
    withLock(ROOT . '/data/.rl.lock', function () use ($path, $bucket, $max, $window) {
        $d = readJsonFile($path, []);
        $k = $bucket . ':' . clientIp();
        $now = time();
        $arr = isset($d[$k]) && is_array($d[$k]) ? $d[$k] : [];
        $arr = array_values(array_filter($arr, fn($t) => ($now - (int)$t) < $window));
        if (count($arr) >= $max) fail(429, 'تعداد درخواست شما زیاد است. کمی بعد دوباره امتحان کنید.');
        $arr[] = $now;
        $d[$k] = $arr;
        writeJsonFile($path, $d);
    });
}

// ---------- admin ----------
function adminToken(): ?string { return $_COOKIE[ADMIN_COOKIE] ?? null; }
function adminSession(): ?array {
    static $cached = false;
    if ($cached !== false) return $cached;
    $t = adminToken();
    if (!$t) return $cached = null;
    $store = readJsonFile(ROOT . '/data/admin-sessions.json', []);
    if (!isset($store[$t]) || !is_array($store[$t])) return $cached = null;
    $s = $store[$t];
    if (($s['exp'] ?? 0) < time()) {
        unset($store[$t]); writeJsonFile(ROOT . '/data/admin-sessions.json', $store);
        return $cached = null;
    }
    return $cached = $s;
}
function isAdmin(): bool { return adminSession() !== null; }
function adminLabel(): string { $s = adminSession(); return $s ? (string)($s['label'] ?? '') : ''; }
function requireAdmin(): void { if (!isAdmin()) fail(401, 'admin required'); }

// ---------- photo upload ----------
function uploadDir(string $id): string {
    $dir = ROOT . '/assets/uploads/' . $id;
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    return $dir;
}
function deleteUploads(string $id): void {
    $dir = ROOT . '/assets/uploads/' . $id;
    if (!is_dir($dir)) return;
    foreach (glob($dir . '/*') as $f) @unlink($f);
    @rmdir($dir);
}
function handlePhoto(string $id, array $limits): ?string {
    if (!isset($_FILES['photo']) || ($_FILES['photo']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return null;
    $f = $_FILES['photo'];
    if ($f['error'] !== UPLOAD_ERR_OK) fail(400, 'خطا در آپلود عکس');
    if ($f['size'] > (int)($limits['maxPhotoSizeKB'] ?? 1024) * 1024) fail(400, 'حجم عکس بیش از حد مجاز');
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($f['tmp_name']);
    $allowed = $limits['allowedImageTypes'] ?? ['image/jpeg', 'image/png', 'image/webp'];
    if (!in_array($mime, $allowed, true)) fail(400, 'نوع عکس مجاز نیست');
    $ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'][$mime] ?? 'jpg';
    $dir = uploadDir($id);
    $rel = 'assets/uploads/' . $id . '/photo.' . $ext;
    $dst = ROOT . '/' . $rel;
    // remove any previous file
    foreach (glob($dir . '/photo.*') as $old) @unlink($old);
    if (!move_uploaded_file($f['tmp_name'], $dst)) fail(500, 'خطا در ذخیره عکس');
    return $rel;
}

// ---------- normalize + public view ----------
function normalizeContact(array $c): array {
    $f = fn($k) => isset($c[$k]) ? trimStr((string)$c[$k]) : '';
    // strip leading @ from handle-like fields
    $h = function ($k) use ($c) {
        $v = isset($c[$k]) ? trimStr((string)$c[$k], 80) : '';
        return ltrim($v, '@');
    };
    return [
        'id'          => isset($c['id']) ? trimStr((string)$c['id'], 32) : newId(),
        'firstName'   => $f('firstName'),
        'lastName'    => $f('lastName'),
        'role'        => $f('role'),
        'department'  => $f('department'),
        'landline'    => $f('landline'),
        'internal'    => $f('internal'),
        'phone'       => $f('phone'),
        'telegram'    => $h('telegram'),
        'rubika'      => $h('rubika'),
        'eitaa'       => $h('eitaa'),
        'bale'        => $h('bale'),
        'whatsapp'    => $h('whatsapp'),
        'email'       => $f('email'),
        'description' => isset($c['description']) ? trimStr((string)$c['description'], 800) : '',
        'photo'       => isset($c['photo']) ? trimStr((string)$c['photo'], 200) : '',
        'status'      => in_array($c['status'] ?? 'pending', ['pending','approved','rejected'], true) ? $c['status'] : 'pending',
        'submittedBy' => in_array($c['submittedBy'] ?? 'user', ['user','admin'], true) ? $c['submittedBy'] : 'user',
        'suggestedBy' => isset($c['suggestedBy']) && is_array($c['suggestedBy']) ? $c['suggestedBy'] : null,
        'createdAt'   => (int)($c['createdAt'] ?? time()),
        'updatedAt'   => (int)($c['updatedAt'] ?? ($c['createdAt'] ?? time())),
    ];
}

function publicView(array $c): array {
    // remove internal-only fields
    $out = $c;
    unset($out['suggestedBy']);
    // drop empty strings to keep payload compact
    foreach (['firstName','lastName','role','department','landline','internal','phone',
              'telegram','rubika','eitaa','bale','whatsapp','email','description','photo'] as $k) {
        if (($out[$k] ?? '') === '') unset($out[$k]);
    }
    return $out;
}

// ---------- actions ----------
$action = $_GET['action'] ?? '';
$config = readJsonFile(ROOT . '/data/config.json', []);

switch ($action) {

case 'config': {
    // public config — omit admin hashes
    $out = $config;
    unset($out['admin']);
    ok($out);
}

case 'list': {
    // Return all approved contacts (sorted by name). Client does search/pagination.
    $data = readJsonFile(ROOT . '/data/contacts.json', ['contacts' => []]);
    $items = [];
    foreach ($data['contacts'] ?? [] as $c) {
        if (($c['status'] ?? '') !== 'approved') continue;
        $items[] = publicView($c);
    }
    // stable sort by (lastName, firstName)
    usort($items, function ($a, $b) {
        $al = ($a['lastName'] ?? '') . ' ' . ($a['firstName'] ?? '');
        $bl = ($b['lastName'] ?? '') . ' ' . ($b['firstName'] ?? '');
        return strcmp($al, $bl);
    });
    ok(['contacts' => $items, 'total' => count($items)]);
}

case 'suggest': {
    // Public contact submission (goes to admin pending queue)
    requireMethod('POST');
    rateLimit('suggest', (int)($config['limits']['maxSubmitsPerHour'] ?? 5));

    $id = newId();
    $c = [
        'id'          => $id,
        'firstName'   => (string)input('firstName', ''),
        'lastName'    => (string)input('lastName', ''),
        'role'        => (string)input('role', ''),
        'department'  => (string)input('department', ''),
        'landline'    => (string)input('landline', ''),
        'internal'    => (string)input('internal', ''),
        'phone'       => (string)input('phone', ''),
        'telegram'    => (string)input('telegram', ''),
        'rubika'      => (string)input('rubika', ''),
        'eitaa'       => (string)input('eitaa', ''),
        'bale'        => (string)input('bale', ''),
        'whatsapp'    => (string)input('whatsapp', ''),
        'email'       => (string)input('email', ''),
        'description' => (string)input('description', ''),
        'status'      => 'pending',
        'submittedBy' => 'user',
        'suggestedBy' => [
            'name'    => trimStr((string)input('subName', ''), 80),
            'contact' => trimStr((string)input('subContact', ''), 80),
            'ip'      => clientIp(),
        ],
        'createdAt'   => time(),
    ];
    $c = normalizeContact($c);
    if ($c['firstName'] === '' && $c['lastName'] === '') fail(400, 'نام یا نام‌خانوادگی الزامی است');
    if ($c['landline'] === '' && $c['phone'] === '' && $c['telegram'] === '' && $c['rubika'] === '' && $c['eitaa'] === '' && $c['email'] === '') {
        fail(400, 'حداقل یک راه تماس (ثابت/شخصی/آیدی/ایمیل) لازم است');
    }

    $photo = handlePhoto($id, $config['limits'] ?? []);
    if ($photo) $c['photo'] = $photo;

    withLockedContacts(function ($d) use ($c) {
        $d['contacts'][] = $c;
        return $d;
    });
    ok(['id' => $id, 'message' => 'پیشنهاد شما ثبت شد. پس از تأیید مدیر نمایش داده می‌شود.']);
}

// ---------- admin ----------
case 'admin-login': {
    requireMethod('POST');
    $pw = (string)input('password', '');
    $candidates = [];
    if (!empty($config['admin']['passwords']) && is_array($config['admin']['passwords'])) {
        foreach ($config['admin']['passwords'] as $e) {
            if (!is_array($e)) continue;
            $h = (string)($e['hash'] ?? '');
            if ($h === '') continue;
            $candidates[] = ['label' => (string)($e['label'] ?? 'مدیر'), 'hash' => $h];
        }
    }
    $legacy = (string)($config['admin']['passwordHash'] ?? '');
    if ($legacy !== '') $candidates[] = ['label' => 'مدیر', 'hash' => $legacy];
    if (!$candidates) fail(500, 'admin not configured');

    $match = null;
    foreach ($candidates as $cand) {
        if (password_verify($pw, $cand['hash'])) { $match = $cand; break; }
    }
    if (!$match) { usleep(400000); fail(401, 'پسورد نادرست است'); }

    $token = newToken();
    $store = readJsonFile(ROOT . '/data/admin-sessions.json', []);
    $now = time();
    foreach ($store as $k => $v) {
        if (!is_array($v) || (($v['exp'] ?? 0) <= $now)) unset($store[$k]);
    }
    $store[$token] = [
        'label' => $match['label'],
        'exp'   => $now + 8 * 3600,
        'ip'    => clientIp(),
        'loginAt' => $now,
    ];
    writeJsonFile(ROOT . '/data/admin-sessions.json', $store);
    setcookie(ADMIN_COOKIE, $token, [
        'expires'  => $now + 8 * 3600,
        'path'     => '/',
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
    ok(['message' => 'ورود موفق', 'label' => $match['label']]);
}

case 'admin-logout': {
    requireMethod('POST');
    $t = adminToken();
    if ($t) {
        $store = readJsonFile(ROOT . '/data/admin-sessions.json', []);
        unset($store[$t]);
        writeJsonFile(ROOT . '/data/admin-sessions.json', $store);
    }
    setcookie(ADMIN_COOKIE, '', ['expires' => time() - 3600, 'path' => '/']);
    ok(['message' => 'خروج انجام شد']);
}

case 'admin-status': {
    ok(['admin' => isAdmin(), 'label' => adminLabel()]);
}

case 'admin-list': {
    requireAdmin();
    $status = $_GET['status'] ?? 'all';
    $data = readJsonFile(ROOT . '/data/contacts.json', ['contacts' => []]);
    $items = [];
    foreach ($data['contacts'] ?? [] as $c) {
        if ($status !== 'all' && ($c['status'] ?? '') !== $status) continue;
        $items[] = $c;
    }
    usort($items, fn($a, $b) => ($b['createdAt'] ?? 0) <=> ($a['createdAt'] ?? 0));
    ok(['contacts' => $items, 'count' => count($items)]);
}

case 'admin-save': {
    // Admin creates or edits a contact directly (approved by default)
    requireAdmin();
    requireMethod('POST');
    $id = trimStr((string)input('id', ''), 32);
    $isNew = ($id === '');
    if ($isNew) $id = newId();

    $c = [
        'id'          => $id,
        'firstName'   => (string)input('firstName', ''),
        'lastName'    => (string)input('lastName', ''),
        'role'        => (string)input('role', ''),
        'department'  => (string)input('department', ''),
        'landline'    => (string)input('landline', ''),
        'internal'    => (string)input('internal', ''),
        'phone'       => (string)input('phone', ''),
        'telegram'    => (string)input('telegram', ''),
        'rubika'      => (string)input('rubika', ''),
        'eitaa'       => (string)input('eitaa', ''),
        'bale'        => (string)input('bale', ''),
        'whatsapp'    => (string)input('whatsapp', ''),
        'email'       => (string)input('email', ''),
        'description' => (string)input('description', ''),
        'status'      => 'approved',
        'submittedBy' => 'admin',
        'createdAt'   => time(),
    ];
    $c = normalizeContact($c);
    if ($c['firstName'] === '' && $c['lastName'] === '') fail(400, 'نام یا نام‌خانوادگی الزامی است');

    $photo = handlePhoto($id, $config['limits'] ?? []);

    withLockedContacts(function ($d) use ($c, $isNew, $photo) {
        $updated = false;
        foreach ($d['contacts'] as $i => $old) {
            if (($old['id'] ?? '') === $c['id']) {
                // preserve createdAt & existing photo if no new one
                $c['createdAt'] = (int)($old['createdAt'] ?? $c['createdAt']);
                if (!$photo && !empty($old['photo'])) $c['photo'] = $old['photo'];
                elseif ($photo) $c['photo'] = $photo;
                $c['updatedAt'] = time();
                $d['contacts'][$i] = $c;
                $updated = true;
                break;
            }
        }
        if (!$updated) {
            if ($photo) $c['photo'] = $photo;
            $c['updatedAt'] = $c['createdAt'];
            $d['contacts'][] = $c;
        }
        return $d;
    });
    ok(['id' => $c['id'], 'message' => $isNew ? 'مخاطب جدید اضافه شد' : 'مخاطب بروزرسانی شد']);
}

case 'admin-decision': {
    requireAdmin();
    requireMethod('POST');
    $id = trimStr((string)input('id', ''), 32);
    $d = trimStr((string)input('decision', ''), 20);
    if (!$id || !in_array($d, ['approve','reject','delete'], true)) fail(400, 'bad request');

    $deletedId = null;
    withLockedContacts(function ($data) use ($id, $d, &$deletedId) {
        $new = [];
        foreach ($data['contacts'] as $c) {
            if (($c['id'] ?? '') !== $id) { $new[] = $c; continue; }
            if ($d === 'delete') { $deletedId = $id; continue; }
            $c['status'] = $d === 'approve' ? 'approved' : 'rejected';
            $c['updatedAt'] = time();
            $new[] = $c;
        }
        $data['contacts'] = $new;
        return $data;
    });
    if ($deletedId) deleteUploads($deletedId);
    ok(['message' => 'انجام شد']);
}

default:
    fail(404, 'unknown action');
}
