<?php

declare(strict_types=1);

function generate_id(): string
{
    return bin2hex(random_bytes(16));
}

function ensure_csv_file(string $path, array $headers): void
{
    if (file_exists($path)) {
        return;
    }

    $dir = dirname($path);
    if (!is_dir($dir)) {
        mkdir($dir, 0777, true);
    }

    $fh = fopen($path, 'wb');
    if ($fh === false) {
        throw new RuntimeException('Unable to create CSV: ' . $path);
    }

    fputcsv($fh, $headers);
    fclose($fh);
}

/**
 * @return array<int, array<string, string>>
 */
function csv_read_assoc(string $path, array $expectedHeaders): array
{
    ensure_csv_file($path, $expectedHeaders);

    $fh = fopen($path, 'rb');
    if ($fh === false) {
        throw new RuntimeException('Unable to read CSV: ' . $path);
    }

    $headers = fgetcsv($fh);
    if (!is_array($headers) || empty($headers)) {
        $headers = $expectedHeaders;
    }

    $rows = [];
    while (($data = fgetcsv($fh)) !== false) {
        if ($data === [null] || $data === []) {
            continue;
        }

        $row = [];
        foreach ($headers as $i => $key) {
            $row[(string)$key] = isset($data[$i]) ? (string)$data[$i] : '';
        }
        $rows[] = $row;
    }

    fclose($fh);
    return $rows;
}

/**
 * @param array<int, array<string, string|int|float>> $rows
 */
function csv_write_assoc(string $path, array $headers, array $rows): void
{
    ensure_csv_file($path, $headers);

    $fh = fopen($path, 'c+');
    if ($fh === false) {
        throw new RuntimeException('Unable to open CSV for writing: ' . $path);
    }

    if (!flock($fh, LOCK_EX)) {
        fclose($fh);
        throw new RuntimeException('Unable to lock CSV: ' . $path);
    }

    ftruncate($fh, 0);
    rewind($fh);

    fputcsv($fh, $headers);

    foreach ($rows as $row) {
        $line = [];
        foreach ($headers as $h) {
            $line[] = isset($row[$h]) ? (string)$row[$h] : '';
        }
        fputcsv($fh, $line);
    }

    fflush($fh);
    flock($fh, LOCK_UN);
    fclose($fh);
}

/**
 * @param array<string, string|int|float> $row
 */
function csv_append_assoc(string $path, array $headers, array $row): void
{
    ensure_csv_file($path, $headers);

    $fh = fopen($path, 'ab');
    if ($fh === false) {
        throw new RuntimeException('Unable to open CSV for append: ' . $path);
    }

    if (!flock($fh, LOCK_EX)) {
        fclose($fh);
        throw new RuntimeException('Unable to lock CSV: ' . $path);
    }

    $line = [];
    foreach ($headers as $h) {
        $line[] = isset($row[$h]) ? (string)$row[$h] : '';
    }
    fputcsv($fh, $line);

    fflush($fh);
    flock($fh, LOCK_UN);
    fclose($fh);
}

/**
 * @param array<int, array<string, string>> $rows
 * @return array<int, array<string, string>>
 */
function csv_filter(array $rows, callable $predicate): array
{
    $out = [];
    foreach ($rows as $row) {
        if ($predicate($row)) {
            $out[] = $row;
        }
    }
    return $out;
}

/**
 * @param array<int, array<string, string>> $rows
 * @return array<string, string>|null
 */
function csv_find_one(array $rows, callable $predicate): ?array
{
    foreach ($rows as $row) {
        if ($predicate($row)) {
            return $row;
        }
    }
    return null;
}
