<?php

require_once __DIR__ . '/utils.php';

/**
 * @param mixed $raw
 * @return array<int, array<string, mixed>>
 */
function normalizeStoresList($raw): array {
    if (!is_array($raw) || !array_is_list($raw)) {
        return [];
    }
    $out = [];
    foreach ($raw as $row) {
        if (is_array($row) && !empty($row['id']) && is_string($row['id'])) {
            $out[] = $row;
        }
    }
    return $out;
}

/**
 * @param mixed $raw
 * @return array<int, array<string, mixed>>
 */
function normalizeCatalogList($raw): array {
    if (!is_array($raw) || !array_is_list($raw)) {
        return [];
    }
    $out = [];
    foreach ($raw as $row) {
        if (is_array($row) && !empty($row['id']) && is_string($row['id'])) {
            $out[] = $row;
        }
    }
    return $out;
}

function groceryCatalogDedupeKey(string $storeId, string $name, string $size): string {
    $n = familyHubStrLowerUtf8(trim($name));
    $s = familyHubStrLowerUtf8(trim($size));
    return $storeId . '|' . $n . '|' . $s;
}

/**
 * @return array{byStore: array<string, array<int, array<string, mixed>>>}
 */
function defaultGroceryListsShape(): array {
    return ['byStore' => []];
}

/**
 * @param mixed $raw
 * @return array{byStore: array<string, array<int, array<string, mixed>>>}
 */
function normalizeGroceryLists($raw): array {
    $base = defaultGroceryListsShape();
    if (!is_array($raw)) {
        return $base;
    }
    if (isset($raw['byStore']) && is_array($raw['byStore'])) {
        $base['byStore'] = [];
        foreach ($raw['byStore'] as $storeId => $items) {
            $sid = (string) $storeId;
            if ($sid === '') {
                continue;
            }
            if (!is_array($items) || !array_is_list($items)) {
                $base['byStore'][$sid] = [];
                continue;
            }
            $clean = [];
            foreach ($items as $it) {
                if (is_array($it) && !empty($it['id']) && is_string($it['id'])) {
                    $clean[] = $it;
                }
            }
            $base['byStore'][$sid] = $clean;
        }
        return $base;
    }
    return $base;
}

/**
 * @param array<int, array<string, mixed>> $stores
 */
function findStoreById(array $stores, string $id): ?array {
    foreach ($stores as $s) {
        if (($s['id'] ?? '') === $id) {
            return $s;
        }
    }
    return null;
}

/**
 * @param array<int, array<string, mixed>> $catalog
 */
function findCatalogById(array $catalog, string $id): ?array {
    foreach ($catalog as $c) {
        if (($c['id'] ?? '') === $id) {
            return $c;
        }
    }
    return null;
}

/**
 * Deduped rows for picker: one per dedupeKey (first wins for label).
 *
 * @param array<int, array<string, mixed>> $catalog
 * @return array<int, array<string, mixed>>
 */
function groceryCatalogPickerOptions(array $catalog, ?string $storeId = null): array {
    $seen = [];
    $out = [];
    foreach ($catalog as $row) {
        if ($storeId !== null && (string) ($row['storeId'] ?? '') !== $storeId) {
            continue;
        }
        $key = (string) ($row['dedupeKey'] ?? '');
        if ($key === '') {
            $key = groceryCatalogDedupeKey(
                (string) ($row['storeId'] ?? ''),
                (string) ($row['name'] ?? ''),
                (string) ($row['defaultSize'] ?? '')
            );
        }
        if (isset($seen[$key])) {
            continue;
        }
        $seen[$key] = true;
        $out[] = $row;
    }
    usort($out, static function ($a, $b) {
        return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
    });
    return $out;
}

/**
 * Migrate legacy groceries.json flat list into stores + lists + empty catalog.
 *
 * @return array{stores: array<int, array<string, mixed>>, lists: array{byStore: array<string, array<int, array<string, mixed>>>}, migrated: bool}
 */
function migrateLegacyGroceriesIfNeeded(): array {
    $listsPath = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
    $hasNew = count($listsPath['byStore']) > 0;
    $legacy = readJsonFile('groceries.json');
    $legacyList = is_array($legacy) && array_is_list($legacy) ? $legacy : [];

    if ($hasNew || count($legacyList) === 0) {
        return ['stores' => normalizeStoresList(readJsonFile('stores.json')), 'lists' => $listsPath, 'migrated' => false];
    }

    $storeId = bin2hex(random_bytes(8));
    $stores = [[
        'id' => $storeId,
        'name' => 'General',
        'sort' => 0,
    ]];

    $items = [];
    foreach ($legacyList as $row) {
        if (!is_array($row)) {
            continue;
        }
        $name = trim((string) ($row['name'] ?? ''));
        if ($name === '') {
            continue;
        }
        $cat = trim((string) ($row['category'] ?? ''));
        $desc = trim((string) ($row['description'] ?? ''));
        if ($cat !== '' && $desc !== '') {
            $desc = 'Category: ' . $cat . "\n" . $desc;
        } elseif ($cat !== '') {
            $desc = 'Category: ' . $cat;
        }

        $items[] = [
            'id' => bin2hex(random_bytes(8)),
            'catalogId' => null,
            'name' => $name,
            'description' => $desc,
            'size' => '',
            'quantity' => (string) ($row['quantity'] ?? '1'),
            'price' => '',
            'image' => '',
            'status' => 'active',
            'purchasedAt' => null,
            'source' => 'manual',
            'recurringIntervalDays' => 0,
            'addedAt' => gmdate('c'),
        ];
    }

    $listsPath['byStore'][$storeId] = $items;

    writeJsonFile('stores.json', $stores);
    writeJsonFile('grocery_lists.json', $listsPath);

    return ['stores' => $stores, 'lists' => $listsPath, 'migrated' => true];
}

/**
 * Append one shopping line using already-loaded catalog and lists (mutates arrays).
 *
 * @param array<int, array<string, mixed>> $catalog
 * @param array{byStore: array<string, array<int, array<string, mixed>>>} $lists
 * @param array<int, array<string, mixed>> $stores
 * @return array{ok: bool, item?: array<string, mixed>, error?: string}
 */
function groceryAppendShoppingLineIntoBuffers(
    array &$catalog,
    array &$lists,
    array $stores,
    string $storeId,
    string $name,
    string $description = '',
    string $size = '',
    string $quantity = '1',
    string $price = '',
    string $image = '',
    string $source = 'manual',
    int $recurringIntervalDays = 0,
    ?string $mealId = null,
    ?string $mealTitleMeta = null
): array {
    if (findStoreById($stores, $storeId) === null) {
        return ['ok' => false, 'error' => 'Invalid store'];
    }
    $name = trim($name);
    if ($name === '') {
        return ['ok' => false, 'error' => 'Name is required'];
    }
    $description = trim($description);
    $size = trim($size);
    $quantity = trim($quantity) !== '' ? trim($quantity) : '1';
    $price = trim($price);
    $image = trim($image);

    $dedupeKey = groceryCatalogDedupeKey($storeId, $name, $size);
    $catalogId = null;
    foreach ($catalog as $i => $c) {
        if ((string) ($c['storeId'] ?? '') === $storeId && (string) ($c['dedupeKey'] ?? '') === $dedupeKey) {
            $catalogId = (string) $c['id'];
            $catalog[$i]['name'] = $name;
            $catalog[$i]['description'] = $description;
            $catalog[$i]['defaultSize'] = $size;
            $catalog[$i]['defaultImage'] = $image;
            $catalog[$i]['dedupeKey'] = $dedupeKey;
            break;
        }
    }
    if ($catalogId === null) {
        $catalogId = bin2hex(random_bytes(8));
        $catalog[] = [
            'id' => $catalogId,
            'storeId' => $storeId,
            'dedupeKey' => $dedupeKey,
            'name' => $name,
            'description' => $description,
            'defaultSize' => $size,
            'defaultImage' => $image,
            'lastPurchaseAt' => null,
            'recurringIntervalDays' => 0,
            'nextDueDate' => null,
        ];
    }

    $status = 'active';
    if ($source === 'meal_plan' || $source === 'pending_review') {
        $status = 'pending_review';
    }
    $src = $source === 'pending_review' ? 'meal_plan' : $source;
    if (!in_array($src, ['manual', 'meal_plan'], true)) {
        $src = 'manual';
    }

    $line = normalizeGroceryLineItem([
        'id' => bin2hex(random_bytes(8)),
        'catalogId' => $catalogId,
        'name' => $name,
        'description' => $description,
        'size' => $size,
        'quantity' => $quantity,
        'price' => $price,
        'image' => $image,
        'status' => $status,
        'purchasedAt' => null,
        'source' => $src,
        'recurringIntervalDays' => max(0, $recurringIntervalDays),
        'addedAt' => gmdate('c'),
    ]);

    if ($mealId !== null && $mealId !== '') {
        $line['mealId'] = $mealId;
    }
    if ($mealTitleMeta !== null && $mealTitleMeta !== '') {
        $line['mealTitle'] = $mealTitleMeta;
    }

    if (!isset($lists['byStore'][$storeId])) {
        $lists['byStore'][$storeId] = [];
    }
    $lists['byStore'][$storeId][] = $line;

    return ['ok' => true, 'item' => $line];
}

/**
 * Create catalog row + list line (used by grocery_item_create and meal → grocery).
 *
 * @param array<int, array<string, mixed>> $stores
 * @return array{ok: bool, item?: array<string, mixed>, error?: string}
 */
function groceryAppendShoppingLine(
    array $stores,
    string $storeId,
    string $name,
    string $description = '',
    string $size = '',
    string $quantity = '1',
    string $price = '',
    string $image = '',
    string $source = 'manual',
    int $recurringIntervalDays = 0,
    ?string $mealId = null,
    ?string $mealTitleMeta = null
): array {
    $catalog = normalizeCatalogList(readJsonFile('grocery_catalog.json'));
    $lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));

    $res = groceryAppendShoppingLineIntoBuffers(
        $catalog,
        $lists,
        $stores,
        $storeId,
        $name,
        $description,
        $size,
        $quantity,
        $price,
        $image,
        $source,
        $recurringIntervalDays,
        $mealId,
        $mealTitleMeta
    );

    if (!$res['ok']) {
        return $res;
    }

    if (!writeJsonFile('grocery_catalog.json', $catalog)) {
        return ['ok' => false, 'error' => 'Failed to save catalog'];
    }
    if (!writeJsonFile('grocery_lists.json', $lists)) {
        return ['ok' => false, 'error' => 'Failed to save grocery list'];
    }

    return $res;
}

/**
 * Append several meal shopping lines in one disk transaction (catalog + lists).
 *
 * @param array<int, array<string, mixed>> $stores
 * @param list<array{name?:mixed, storeId?:mixed, description?:mixed, size?:mixed, quantity?:mixed, price?:mixed, image?:mixed}> $itemRows
 * @return int number of lines successfully added
 */
function groceryAppendMealItemRowsBatch(
    array $stores,
    array $itemRows,
    string $mealId,
    string $mealTitle
): int {
    if (count($itemRows) === 0) {
        return 0;
    }
    $defaultStore = groceryFirstStoreId($stores);
    if ($defaultStore === '') {
        return 0;
    }

    $catalog = normalizeCatalogList(readJsonFile('grocery_catalog.json'));
    $lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
    $added = 0;

    foreach ($itemRows as $row) {
        if (!is_array($row)) {
            continue;
        }
        $name = trim((string) ($row['name'] ?? ''));
        if ($name === '') {
            continue;
        }
        $sid = trim((string) ($row['storeId'] ?? ''));
        if ($sid === '' || findStoreById($stores, $sid) === null) {
            $sid = $defaultStore;
        }
        $res = groceryAppendShoppingLineIntoBuffers(
            $catalog,
            $lists,
            $stores,
            $sid,
            $name,
            trim((string) ($row['description'] ?? '')),
            trim((string) ($row['size'] ?? '')),
            trim((string) ($row['quantity'] ?? '1')) ?: '1',
            trim((string) ($row['price'] ?? '')),
            trim((string) ($row['image'] ?? '')),
            'meal_plan',
            0,
            $mealId !== '' ? $mealId : null,
            $mealTitle !== '' ? $mealTitle : null
        );
        if ($res['ok']) {
            $added++;
        }
    }

    if ($added === 0) {
        return 0;
    }

    if (!writeJsonFile('grocery_catalog.json', $catalog)) {
        return 0;
    }
    if (!writeJsonFile('grocery_lists.json', $lists)) {
        return 0;
    }

    return $added;
}

/**
 * @param array<int, array<string, mixed>> $stores
 */
function groceryFirstStoreId(array $stores): string {
    if (count($stores) === 0) {
        return '';
    }
    $copy = $stores;
    usort($copy, static function ($a, $b) {
        $sa = (int) ($a['sort'] ?? 0);
        $sb = (int) ($b['sort'] ?? 0);
        if ($sa !== $sb) {
            return $sa <=> $sb;
        }
        return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
    });
    return (string) ($copy[0]['id'] ?? '');
}

/**
 * @param array<string, mixed> $item
 * @return array<string, mixed>
 */
function normalizeGroceryLineItem(array $item): array {
    if (empty($item['id']) || !is_string($item['id'])) {
        $item['id'] = bin2hex(random_bytes(8));
    }
    $item['name'] = trim((string) ($item['name'] ?? ''));
    $item['description'] = trim((string) ($item['description'] ?? ''));
    $item['size'] = trim((string) ($item['size'] ?? ''));
    $item['quantity'] = trim((string) ($item['quantity'] ?? '1'));
    $item['price'] = trim((string) ($item['price'] ?? ''));
    $item['image'] = trim((string) ($item['image'] ?? ''));
    $st = (string) ($item['status'] ?? 'active');
    $item['status'] = in_array($st, ['active', 'purchased', 'pending_review'], true) ? $st : 'active';
    $item['purchasedAt'] = isset($item['purchasedAt']) && is_string($item['purchasedAt']) ? $item['purchasedAt'] : null;
    $item['source'] = in_array($item['source'] ?? 'manual', ['manual', 'meal_plan', 'meal_detail'], true)
        ? $item['source']
        : 'manual';
    $item['recurringIntervalDays'] = max(0, (int) ($item['recurringIntervalDays'] ?? 0));
    $item['catalogId'] = isset($item['catalogId']) && is_string($item['catalogId']) ? $item['catalogId'] : null;
    if (empty($item['addedAt'])) {
        $item['addedAt'] = gmdate('c');
    }
    if (!empty($item['mealId']) && is_string($item['mealId'])) {
        $item['mealId'] = trim($item['mealId']);
    } else {
        unset($item['mealId']);
    }
    if (isset($item['mealTitle']) && is_string($item['mealTitle'])) {
        $item['mealTitle'] = trim($item['mealTitle']);
    } else {
        unset($item['mealTitle']);
    }
    return $item;
}

/**
 * @param array{byStore: array<string, array<int, array<string, mixed>>>} $lists
 */
function groceryStoreHasItems(array $lists, string $storeId): bool {
    $items = $lists['byStore'][$storeId] ?? [];
    return is_array($items) && count($items) > 0;
}

/**
 * If there are no stores yet, create a single "Home" store and empty list bucket.
 */
function ensureDefaultGroceryStore(): void {
    $stores = normalizeStoresList(readJsonFile('stores.json'));
    if (count($stores) > 0) {
        return;
    }
    $id = bin2hex(random_bytes(8));
    writeJsonFile('stores.json', [['id' => $id, 'name' => 'Home', 'sort' => 0]]);
    $lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
    if (!isset($lists['byStore'][$id])) {
        $lists['byStore'][$id] = [];
    }
    writeJsonFile('grocery_lists.json', $lists);
}
