#!/usr/bin/env php
<?php
/**
 * =====================================================================
 *  bin/rx-tool — repair and one-off operations
 *
 *  Usage:
 *    rx-tool rebuild-balances            recompute stock from the ledger
 *    rx-tool check-layout <profile>      validate a print template
 *    rx-tool migration-validate <batch>  check a staged import
 *    rx-tool migration-report <batch>    what came across, and what did not
 *    rx-tool migration-opening <batch>   post the opening journal
 *
 *  =================================================================
 *  WHY THESE ARE NOT ROUTES
 *  =================================================================
 *
 *  Each is either a repair or a once-per-store operation, and putting
 *  them behind a web route would be worse than leaving them out:
 *
 *    · rebuild-balances rewrites every stock figure in the shop. That
 *      is a thing a support engineer does deliberately, on a quiet
 *      afternoon, having first taken a backup — not a button.
 *
 *    · the migration commands run once, during setup, with a person
 *      watching the output. They read a staged import and post an
 *      opening journal; running one twice is not a small mistake.
 *
 *  They were unreachable, which is not the same as unnecessary. A
 *  capability with no home is a capability nobody can use when the
 *  afternoon comes that they need it.
 * =====================================================================
 */
declare(strict_types=1);

$root = dirname(__DIR__);
require $root . '/lib/bootstrap.php';
require $root . '/lib/migration.php';

$cfg = require $root . '/config.php';
$db  = new Db($cfg['dsn'], $cfg['db_user'], $cfg['db_pass']);

$cmd = $argv[1] ?? '';
$arg = $argv[2] ?? '';

/** Every command works on one store, named or the only one there is. */
function pickStore(Db $db, ?string $code): array
{
    $stores = $db->all('SELECT id, tenant_id, code, name FROM store WHERE is_active=1');
    if ($stores === []) { fwrite(STDERR, "no active store on this node\n"); exit(2); }
    if ($code !== null && $code !== '') {
        foreach ($stores as $s) { if ($s['code'] === $code) { return $s; } }
        fwrite(STDERR, "no store with code {$code}\n"); exit(2);
    }
    if (count($stores) > 1) {
        fwrite(STDERR, "this node has several stores — name one: "
             . implode(', ', array_column($stores, 'code')) . "\n");
        exit(2);
    }
    return $stores[0];
}

try {
    switch ($cmd) {

    case 'rebuild-balances':
        $st = pickStore($db, $arg);
        // Loud on purpose. This rewrites every stock figure in the shop,
        // and the right reaction to seeing it run unexpectedly is alarm.
        printf("Rebuilding stock balances for %s (%s) from the ledger.\n", $st['name'], $st['code']);
        printf("Take a backup first if you have not:  php bin/rx-worker run-job backup.run\n\n");
        $n = (new BillingEngine($db, (string) $st['tenant_id'], (string) $st['id'],
                                '', null, new Numbering($db, (string) $st['id'], '')))
             ->rebuildBalances();
        printf("  %d balance(s) recomputed.\n", $n);
        break;

    case 'check-layout':
        if ($arg === '') { fwrite(STDERR, "usage: rx-tool check-layout <profile-code>\n"); exit(1); }
        $p = $db->one('SELECT * FROM print_profile WHERE code=?', [$arg]);
        if ($p === null) { fwrite(STDERR, "no layout with code {$arg}\n"); exit(2); }

        $pe = new PrintEngine(new EscPosEmitter((int) $p['columns']));
        // validate() returns a plain list of problems — empty means
        // valid. I wrote this expecting ['ok'=>..], which reported "0
        // problems" while exiting as a failure. Read the function.
        $problems = $pe->validate((string) $p['template_body']);
        if ($problems === []) {
            printf("  %s is valid (%d columns).\n", $arg, (int) $p['columns']);
            break;
        }
        // A layout fault found here is a slip that would have printed
        // wrong on every bill until somebody noticed.
        printf("  %s has %d problem(s):\n", $arg, count($problems));
        foreach ($problems as $e) { printf("    · %s\n", is_string($e) ? $e : json_encode($e)); }
        exit(1);

    case 'migration-validate':
    case 'migration-report':
    case 'migration-opening':
        if ($arg === '') { fwrite(STDERR, "usage: rx-tool {$cmd} <batch-id>\n"); exit(1); }
        $batch = $db->one('SELECT * FROM import_batch WHERE id=?', [$arg]);
        if ($batch === null) { fwrite(STDERR, "no import batch {$arg}\n"); exit(2); }

        $me = new MigrationEngine($db, (string) $batch['tenant_id'], (string) $batch['store_id']);

        if ($cmd === 'migration-validate') {
            $r = $me->validate($arg);
            printf("  %d row(s) checked, %d problem(s).\n",
                   $r['checked'] ?? 0, $r['errors'] ?? 0);
            break;
        }
        if ($cmd === 'migration-report') {
            $r = $me->reconciliationReport($arg);
            // What did NOT come across is the number that matters. A
            // migration report that only counts successes is a report
            // designed to be shown rather than read.
            foreach ($r as $k => $v) {
                printf("  %-28s %s\n", $k, is_scalar($v) ? (string) $v : json_encode($v));
            }
            break;
        }
        $acct = (new PostingEngine($db, (string) $batch['tenant_id'],
                                   (string) $batch['store_id']))->accounts();
        $r = $me->postOpeningJournal($acct);
        printf("  opening journal posted: %s\n", json_encode($r));
        break;

    default:
        fwrite(STDERR, "usage: rx-tool [rebuild-balances|check-layout|"
                     . "migration-validate|migration-report|migration-opening] <arg>\n");
        exit(1);
    }

} catch (Throwable $e) {
    // Same rule as every other command here: a sentence, not a trace.
    fwrite(STDERR, "\n" . $e->getMessage() . "\n\nSend this to support.\n\n");
    exit(3);
}
