<?php
/* ============================================================
 * Larspeenghar Pedigree — server API
 * Single-file PHP backend for shared cPanel hosting.
 *
 *   Data is stored as JSON files in ./data/  (protected by .htaccess).
 *   Sessions are PHP-native. Passwords are bcrypt-hashed.
 *   Per-owner scoping: non-admin users see only their own animals/owners.
 *
 *   Endpoints (all return JSON):
 *     POST ?action=login        body: {username, password}
 *     POST ?action=logout
 *     GET  ?action=session
 *     GET  ?action=fetch&store=<animals|owners|indiv_owners|messages|settings|users>
 *     POST ?action=save&store=<...>           body: {data: [...]}
 *     POST ?action=save_user                  body: {id, username, password, role, ownerId, name}
 *     POST ?action=delete_user                body: {id}
 * ============================================================ */

session_start();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');

/* ------------------------------------------------------------------
 * DATA LOCATION — kept OUTSIDE the code so upgrades never touch it.
 *
 * The data directory path lives in config.php, which is created once on
 * first run and is NEVER overwritten when you upload new code. To move
 * your data somewhere safer (e.g. above public_html, where a code deploy
 * can't reach it), just edit config.php — no code change needed.
 * ------------------------------------------------------------------ */
$CONFIG_FILE = __DIR__ . '/config.php';
if (file_exists($CONFIG_FILE)) {
    $cfg = include $CONFIG_FILE;
    if (!is_array($cfg)) $cfg = [];
} else {
    // First run: create a default config.php next to the code.
    // Default data dir is ./data; change 'data_dir' to an absolute path
    // OUTSIDE the web root for the safest upgrades, e.g.:
    //   'data_dir' => dirname(__DIR__) . '/pedigree_data',
    $cfg = ['data_dir' => __DIR__ . '/data'];
    $template = "<?php\n"
        . "/* Larspeenghar Pedigree — deployment config.\n"
        . " * This file is created ONCE and is NOT overwritten on upgrade.\n"
        . " * It is safe to edit. To keep your data out of reach of code\n"
        . " * deploys, point 'data_dir' at a folder ABOVE your web root:\n"
        . " *   'data_dir' => dirname(__DIR__) . '/pedigree_data',\n"
        . " */\n"
        . "return [\n"
        . "    'data_dir' => __DIR__ . '/data',\n"
        . "];\n";
    @file_put_contents($CONFIG_FILE, $template);
}

$DATA_DIR = isset($cfg['data_dir']) && $cfg['data_dir'] !== ''
    ? $cfg['data_dir']
    : __DIR__ . '/data';

if (!is_dir($DATA_DIR)) {
    @mkdir($DATA_DIR, 0700, true);
}
// Belt-and-braces: drop an .htaccess inside the data dir if it lives under
// the web root, in case the site-root .htaccess is missed. (Harmless if the
// dir is outside the web root — Apache just never reads it there.)
if (!file_exists($DATA_DIR . '/.htaccess')) {
    @file_put_contents($DATA_DIR . '/.htaccess', "Order allow,deny\nDeny from all\nRequire all denied\n");
}

/* ---------- helpers ---------- */
function fail($code, $msg) {
    http_response_code($code);
    echo json_encode(['error' => $msg]);
    exit;
}

function read_json($file, $default = []) {
    global $DATA_DIR;
    $path = "$DATA_DIR/$file";
    if (!file_exists($path)) return $default;
    $fp = @fopen($path, 'r');
    if (!$fp) return $default;
    @flock($fp, LOCK_SH);
    $size = filesize($path);
    $raw = $size > 0 ? fread($fp, $size) : '';
    @flock($fp, LOCK_UN);
    fclose($fp);
    $data = json_decode($raw, true);
    return is_array($data) ? $data : $default;
}

function write_json($file, $data) {
    global $DATA_DIR;
    $path = "$DATA_DIR/$file";
    $fp = @fopen($path, 'c+');
    if (!$fp) fail(500, 'Cannot open storage file');
    @flock($fp, LOCK_EX);
    ftruncate($fp, 0);
    rewind($fp);
    fwrite($fp, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    fflush($fp);
    @flock($fp, LOCK_UN);
    fclose($fp);
}

function current_user() {
    if (empty($_SESSION['userId'])) return null;
    $users = read_json('users.json', []);
    foreach ($users as $u) {
        if (($u['id'] ?? null) === $_SESSION['userId']) return $u;
    }
    return null;
}

function require_login() {
    $u = current_user();
    if (!$u) fail(401, 'Not authenticated');
    return $u;
}

function strip_pw($u) {
    if (is_array($u)) unset($u['password']);
    return $u;
}

/* ---------- bootstrap default admin ---------- */
$users = read_json('users.json', []);
if (!$users) {
    $users = [[
        'id'       => 'admin',
        'username' => 'admin',
        'password' => password_hash('larspeenghar2024', PASSWORD_DEFAULT),
        'role'     => 'admin',
        'ownerId'  => null,
        'name'     => 'Administrator',
    ]];
    write_json('users.json', $users);
}

/* ---------- routing ---------- */
$action = $_GET['action'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
$input  = [];
if ($method === 'POST') {
    $raw = file_get_contents('php://input');
    $input = $raw ? (json_decode($raw, true) ?: []) : [];
}

switch ($action) {

    /* ---- AUTH ---- */
    case 'login': {
        if ($method !== 'POST') fail(405, 'POST required');
        $uname = strtolower(trim($input['username'] ?? ''));
        $pass  = (string)($input['password'] ?? '');
        if ($uname === '' || $pass === '') fail(400, 'Username and password required');

        $found = null;
        foreach ($users as $u) {
            if (strtolower($u['username'] ?? '') !== $uname) continue;
            // Allow either bcrypt or legacy plaintext (one-time migration).
            $stored = $u['password'] ?? '';
            $ok = false;
            if (str_starts_with($stored, '$2y$') || str_starts_with($stored, '$2b$') || str_starts_with($stored, '$argon')) {
                $ok = password_verify($pass, $stored);
            } else {
                $ok = hash_equals($stored, $pass);
                if ($ok) {
                    // Upgrade to hash on successful legacy login
                    $u['password'] = password_hash($pass, PASSWORD_DEFAULT);
                    foreach ($users as $i => $uu) if (($uu['id'] ?? '') === ($u['id'] ?? '')) { $users[$i] = $u; break; }
                    write_json('users.json', $users);
                }
            }
            if ($ok) { $found = $u; break; }
        }
        if (!$found) fail(401, 'Invalid username or password');

        session_regenerate_id(true);
        $_SESSION['userId'] = $found['id'];
        echo json_encode(['user' => strip_pw($found)]);
        break;
    }

    case 'logout': {
        $_SESSION = [];
        if (ini_get('session.use_cookies')) {
            $p = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
        }
        session_destroy();
        echo json_encode(['ok' => true]);
        break;
    }

    case 'session': {
        $u = current_user();
        echo json_encode(['user' => $u ? strip_pw($u) : null]);
        break;
    }

    /* ---- DATA ---- */
    case 'fetch': {
        $me = require_login();
        $store = $_GET['store'] ?? '';
        $allowed = ['animals', 'owners', 'indiv_owners', 'messages', 'settings', 'settings_i18n', 'users'];
        if (!in_array($store, $allowed, true)) fail(400, 'Bad store');

        $data = read_json("$store.json", []);

        // Per-role scoping
        $isAdmin = ($me['role'] ?? '') === 'admin';
        $myOwnerId = $me['ownerId'] ?? null;

        if (!$isAdmin) {
            if ($store === 'animals') {
                $data = array_values(array_filter($data, fn($a) => ($a['ownerId'] ?? null) === $myOwnerId));
            } elseif ($store === 'owners') {
                $data = array_values(array_filter($data, fn($o) => ($o['id'] ?? null) === $myOwnerId));
            } elseif ($store === 'users') {
                $data = array_values(array_filter($data, fn($u) => ($u['id'] ?? null) === ($me['id'] ?? null)));
            } elseif ($store === 'messages') {
                $myId = $me['id'] ?? null;
                $data = array_values(array_filter(
                    $data,
                    fn($m) => ($m['from'] ?? null) === $myId || ($m['to'] ?? null) === $myId
                ));
            }
        }

        if ($store === 'users') {
            $data = array_map('strip_pw', $data);
        }

        echo json_encode(['data' => $data]);
        break;
    }

    case 'save': {
        $me = require_login();
        if ($method !== 'POST') fail(405, 'POST required');
        $store = $_GET['store'] ?? '';
        $allowed = ['animals', 'owners', 'indiv_owners', 'messages', 'settings', 'settings_i18n'];
        if (!in_array($store, $allowed, true)) fail(400, 'Bad store');

        $incoming = $input['data'] ?? [];
        if (!is_array($incoming)) fail(400, 'data must be an array or object');

        $isAdmin = ($me['role'] ?? '') === 'admin';

        if ($store === 'settings' || $store === 'settings_i18n') {
            // Object stores, admin-only
            if (!$isAdmin) fail(403, 'Admin only');
            write_json("$store.json", $incoming);
            echo json_encode(['ok' => true]);
            break;
        }

        if ($isAdmin) {
            write_json("$store.json", $incoming);
            echo json_encode(['ok' => true]);
            break;
        }

        // Non-admin: merge — preserve other users' records.
        $myOwnerId = $me['ownerId'] ?? null;
        $myId = $me['id'] ?? null;
        $existing = read_json("$store.json", []);

        if ($store === 'animals') {
            $others = array_values(array_filter($existing, fn($a) => ($a['ownerId'] ?? null) !== $myOwnerId));
            $mine = array_values(array_filter($incoming, fn($a) => ($a['ownerId'] ?? null) === $myOwnerId));
            write_json("$store.json", array_merge($others, $mine));
        } elseif ($store === 'messages') {
            $others = array_values(array_filter(
                $existing,
                fn($m) => ($m['from'] ?? null) !== $myId && ($m['to'] ?? null) !== $myId
            ));
            $mine = array_values(array_filter(
                $incoming,
                fn($m) => ($m['from'] ?? null) === $myId || ($m['to'] ?? null) === $myId
            ));
            write_json("$store.json", array_merge($others, $mine));
        } else {
            // owners / indiv_owners are admin-managed; non-admin write is a no-op.
            fail(403, 'Admin only');
        }
        echo json_encode(['ok' => true]);
        break;
    }

    /* ---- USERS ---- */
    case 'save_user': {
        $me = require_login();
        if ($method !== 'POST') fail(405, 'POST required');

        $isAdmin = ($me['role'] ?? '') === 'admin';
        $editId = $input['id'] ?? null;

        // Non-admin: can only edit themselves, and only name/password (not role/ownerId).
        if (!$isAdmin && $editId !== ($me['id'] ?? null)) fail(403, 'Forbidden');

        $list = read_json('users.json', []);
        $idx = -1;
        foreach ($list as $i => $u) if (($u['id'] ?? null) === $editId) { $idx = $i; break; }

        if ($idx === -1) {
            if (!$isAdmin) fail(403, 'Forbidden');
            // Create new user (admin only)
            $newU = [
                'id'       => $input['id'] ?? ('u' . time() . '_' . substr(bin2hex(random_bytes(2)), 0, 4)),
                'username' => trim((string)($input['username'] ?? '')),
                'role'     => in_array($input['role'] ?? '', ['admin', 'owner'], true) ? $input['role'] : 'owner',
                'ownerId'  => $input['ownerId'] ?? null,
                'name'     => trim((string)($input['name'] ?? '')),
            ];
            $pw = (string)($input['password'] ?? '');
            if ($pw === '') fail(400, 'Password required for new user');
            if (strlen($pw) < 6) fail(400, 'Password must be at least 6 characters');
            // Uniqueness
            foreach ($list as $u) {
                if (strtolower($u['username'] ?? '') === strtolower($newU['username'])) {
                    fail(409, 'Username already exists');
                }
            }
            $newU['password'] = password_hash($pw, PASSWORD_DEFAULT);
            $list[] = $newU;
        } else {
            // Update existing
            $u = $list[$idx];
            if ($isAdmin) {
                if (isset($input['username'])) $u['username'] = trim((string)$input['username']);
                if (isset($input['role'])) $u['role'] = in_array($input['role'], ['admin', 'owner'], true) ? $input['role'] : $u['role'];
                if (array_key_exists('ownerId', $input)) $u['ownerId'] = $input['ownerId'];
            }
            if (isset($input['name'])) $u['name'] = trim((string)$input['name']);
            if (!empty($input['password'])) {
                if (strlen($input['password']) < 6) fail(400, 'Password must be at least 6 characters');
                $u['password'] = password_hash((string)$input['password'], PASSWORD_DEFAULT);
            }
            $list[$idx] = $u;
        }
        write_json('users.json', $list);
        echo json_encode(['ok' => true]);
        break;
    }

    case 'delete_user': {
        $me = require_login();
        if ($method !== 'POST') fail(405, 'POST required');
        if (($me['role'] ?? '') !== 'admin') fail(403, 'Admin only');
        $id = $input['id'] ?? '';
        if ($id === 'admin') fail(400, 'Cannot delete the default admin account');
        $list = read_json('users.json', []);
        $list = array_values(array_filter($list, fn($u) => ($u['id'] ?? null) !== $id));
        write_json('users.json', $list);
        echo json_encode(['ok' => true]);
        break;
    }

    /* ---- FULL BACKUP / RESTORE (admin only) ---- */
    case 'export_all': {
        $me = require_login();
        if (($me['role'] ?? '') !== 'admin') fail(403, 'Admin only');
        // Bundle every store into one portable file. Passwords are included
        // (hashed) so a restore fully re-creates logins.
        $bundle = [
            '_meta' => [
                'app'      => 'Larspeenghar Pedigree Manager',
                'kind'     => 'full-server-backup',
                'version'  => '7.0',
                'exported' => date('c'),
            ],
            'animals'      => read_json('animals.json', []),
            'owners'       => read_json('owners.json', []),
            'indiv_owners' => read_json('indiv_owners.json', []),
            'messages'     => read_json('messages.json', []),
            'settings'     => read_json('settings.json', []),
            'users'        => read_json('users.json', []),
        ];
        echo json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        break;
    }

    case 'import_all': {
        $me = require_login();
        if ($method !== 'POST') fail(405, 'POST required');
        if (($me['role'] ?? '') !== 'admin') fail(403, 'Admin only');
        $b = $input['data'] ?? $input;
        if (!is_array($b)) fail(400, 'Invalid backup payload');

        // Restore each store that is present. Absent keys are left untouched.
        $restored = [];
        foreach (['animals', 'owners', 'indiv_owners', 'messages'] as $s) {
            if (isset($b[$s]) && is_array($b[$s])) {
                write_json("$s.json", array_values($b[$s]));
                $restored[$s] = count($b[$s]);
            }
        }
        if (isset($b['settings']) && is_array($b['settings'])) {
            write_json('settings.json', $b['settings']);
            $restored['settings'] = 1;
        }
        // Users: only restore if present AND contains at least one admin, so an
        // accidental import can never lock everyone out.
        if (isset($b['users']) && is_array($b['users']) && $b['users']) {
            $hasAdmin = false;
            foreach ($b['users'] as $u) if (($u['role'] ?? '') === 'admin') { $hasAdmin = true; break; }
            if ($hasAdmin) {
                write_json('users.json', array_values($b['users']));
                $restored['users'] = count($b['users']);
            }
        }
        echo json_encode(['ok' => true, 'restored' => $restored]);
        break;
    }

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