<?php

declare(strict_types=1);

/**
 * Migrates territory data from database/myAccessFile.accdb into database/app.sqlite.
 *
 * Platform support
 *   Windows  — PDO ODBC via the Microsoft Access Database Engine driver.
 *              Requires: pdo_odbc extension + Microsoft Access Database Engine
 *   Linux/macOS — mdbtools (`mdb-export` CLI).
 *              Requires: sudo apt install mdbtools  /  brew install mdbtools
 *
 * Usage:
 *   php database/migrate_access_to_sqlite.php             # auto-detect driver
 *   php database/migrate_access_to_sqlite.php --dry-run   # show counts, no writes
 *   php database/migrate_access_to_sqlite.php --driver=odbc
 *   php database/migrate_access_to_sqlite.php --driver=mdbtools
 */

// ── CLI args ───────────────────────────────────────────────────────────────
$args   = $argv ?? [];
$dryRun = in_array('--dry-run', $args, true);

$driverFlag = null;
foreach ($args as $arg) {
    if (str_starts_with($arg, '--driver=')) {
        $driverFlag = substr($arg, strlen('--driver='));
    }
}

// ── Paths ──────────────────────────────────────────────────────────────────
$accessPath = realpath(__DIR__ . '/myAccessFile.accdb');
$sqlitePath = realpath(__DIR__ . '/app.sqlite');

if (!$accessPath) {
    fwrite(STDERR, "ERROR: myAccessFile.accdb not found in " . __DIR__ . "\n");
    exit(1);
}

if (!$sqlitePath) {
    fwrite(STDERR, "ERROR: app.sqlite not found — run the migrations first.\n");
    exit(1);
}

// ── Access reader interface ────────────────────────────────────────────────

interface AccessReader
{
    /** Total number of rows in the given Access table. */
    public function count(string $table): int;

    /** Yield each row as an associative array (string keys, string|null values). */
    public function rows(string $table): iterable;
}

// ── ODBC reader (Windows) ──────────────────────────────────────────────────

final class OdbcAccessReader implements AccessReader
{
    private PDO $pdo;

    public function __construct(string $path)
    {
        $dsn = "odbc:Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq={$path};";
        try {
            $this->pdo = new PDO($dsn, '', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
        } catch (PDOException $e) {
            fwrite(STDERR, "ERROR: Cannot open Access database via ODBC.\n{$e->getMessage()}\n");
            fwrite(STDERR, "Ensure pdo_odbc is enabled and the Microsoft Access Database Engine is installed.\n");
            exit(1);
        }
    }

    public function count(string $table): int
    {
        return (int) $this->pdo->query("SELECT COUNT(*) FROM [{$table}]")->fetchColumn();
    }

    public function rows(string $table): iterable
    {
        yield from $this->pdo->query("SELECT * FROM [{$table}]");
    }
}

// ── mdbtools reader (Linux / macOS) ───────────────────────────────────────

final class MdbToolsAccessReader implements AccessReader
{
    public function __construct(private readonly string $path)
    {
        if (!$this->which('mdb-export')) {
            fwrite(STDERR, "ERROR: mdb-export not found.\n");
            fwrite(STDERR, "Install mdbtools:  sudo apt install mdbtools   (Debian/Ubuntu)\n");
            fwrite(STDERR, "                   brew install mdbtools        (macOS)\n");
            exit(1);
        }
    }

    public function count(string $table): int
    {
        // Row count = line count of mdb-export output minus the header line.
        $cmd    = 'mdb-export ' . escapeshellarg($this->path) . ' ' . escapeshellarg($table) . ' | wc -l';
        $output = shell_exec($cmd);
        return max(0, (int) trim((string) $output) - 1);
    }

    public function rows(string $table): iterable
    {
        $cmd    = 'mdb-export ' . escapeshellarg($this->path) . ' ' . escapeshellarg($table);
        $handle = popen($cmd, 'r');

        if ($handle === false) {
            throw new RuntimeException("Failed to run mdb-export for table '{$table}'.");
        }

        $headers = null;

        while (!feof($handle)) {
            $line = fgets($handle);
            if ($line === false) {
                break;
            }
            $line = rtrim($line, "\r\n");
            if ($line === '') {
                continue;
            }

            $cols = str_getcsv($line);

            if ($headers === null) {
                $headers = $cols;
                continue;
            }

            // Pad short rows (trailing empty columns may be omitted by mdbtools).
            while (count($cols) < count($headers)) {
                $cols[] = '';
            }

            yield array_combine($headers, $cols);
        }

        pclose($handle);
    }

    private function which(string $bin): bool
    {
        return !empty(shell_exec('which ' . escapeshellarg($bin) . ' 2>/dev/null'));
    }
}

// ── Select driver ──────────────────────────────────────────────────────────

$driver = $driverFlag ?? (PHP_OS_FAMILY === 'Windows' ? 'odbc' : 'mdbtools');

echo "Access file:  {$accessPath}\n";
echo "Driver:       {$driver}\n";

$reader = match ($driver) {
    'odbc'     => new OdbcAccessReader($accessPath),
    'mdbtools' => new MdbToolsAccessReader($accessPath),
    default    => throw new InvalidArgumentException("Unknown driver '{$driver}'. Use 'odbc' or 'mdbtools'."),
};

// ── SQLite connection ──────────────────────────────────────────────────────

echo "SQLite file:  {$sqlitePath}\n";

$sqlite = new PDO("sqlite:{$sqlitePath}", null, null, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$sqlite->exec('PRAGMA foreign_keys = OFF');
$sqlite->exec('PRAGMA journal_mode = WAL');
$sqlite->exec('PRAGMA synchronous  = NORMAL');
$sqlite->exec('PRAGMA cache_size   = -16000');

if ($dryRun) {
    echo "\n-- DRY RUN: no data will be written --\n";
}

$now = date('Y-m-d H:i:s');

// ── Normalisation helpers ──────────────────────────────────────────────────

function normaliseString(mixed $v): ?string
{
    return ($v === null || $v === '') ? null : (string) $v;
}

function normaliseInt(mixed $v): ?int
{
    return ($v === null || $v === '') ? null : (int) $v;
}

function normaliseFloat(mixed $v): ?float
{
    return ($v === null || $v === '') ? null : (float) $v;
}

function normaliseDate(mixed $v, string $format = 'Y-m-d'): ?string
{
    if ($v === null || $v === '') {
        return null;
    }
    $ts = strtotime((string) $v);
    return $ts !== false ? date($format, $ts) : null;
}

function normaliseDateTime(mixed $v): ?string
{
    return normaliseDate($v, 'Y-m-d H:i:s');
}

// ── Migration runner ───────────────────────────────────────────────────────

function migrateTable(
    AccessReader $src,
    PDO $dst,
    string $srcTable,
    string $dstTable,
    string $insertSql,
    callable $mapRow,
    bool $dryRun,
    int $batchSize = 500
): void {
    $total = $src->count($srcTable);
    echo "  {$srcTable} → {$dstTable}: {$total} rows";

    if ($dryRun) {
        echo " (skipped — dry run)\n";
        return;
    }

    echo "\n";
    $dst->exec("DELETE FROM [{$dstTable}]");

    $stmt = $dst->prepare($insertSql);
    $dst->beginTransaction();
    $n = 0;

    foreach ($src->rows($srcTable) as $row) {
        $stmt->execute($mapRow($row));
        $n++;

        if ($n % $batchSize === 0) {
            $dst->commit();
            $dst->beginTransaction();
            printf("    %d / %d (%.0f%%)\n", $n, $total, $total > 0 ? ($n / $total) * 100 : 0);
        }
    }

    if ($dst->inTransaction()) {
        $dst->commit();
    }

    printf("    Done: %d rows inserted.\n", $n);
}

// ── 1. Territories ─────────────────────────────────────────────────────────
echo "\n[1/3] Territories\n";
migrateTable(
    $reader,
    $sqlite,
    'Territories',
    'territories',
    'INSERT INTO territories (id, name, description, coordinates, created_at, updated_at)
     VALUES (:id, :name, :description, :coordinates, :created_at, :updated_at)',
    function (array $row) use ($now): array {
        return [
            ':id'          => (int) $row['Id'],
            ':name'        => normaliseString($row['Name']),
            ':description' => normaliseString($row['Description']),
            ':coordinates' => normaliseString($row['Coordinates']),
            ':created_at'  => $now,
            ':updated_at'  => $now,
        ];
    },
    $dryRun
);

// ── 2. Households ──────────────────────────────────────────────────────────
echo "\n[2/3] Households\n";
migrateTable(
    $reader,
    $sqlite,
    'Households',
    'households',
    'INSERT INTO households
        (id, territory_id, address, street_number, street_name,
         latitude, longitude, is_business, do_not_call,
         do_not_call_date, do_not_call_notes, do_not_call_private_notes,
         created_at, updated_at)
     VALUES
        (:id, :territory_id, :address, :street_number, :street_name,
         :latitude, :longitude, :is_business, :do_not_call,
         :do_not_call_date, :do_not_call_notes, :do_not_call_private_notes,
         :created_at, :updated_at)',
    function (array $row) use ($now): array {
        return [
            ':id'                        => (int) $row['Id'],
            ':territory_id'              => (int) $row['TerritoryId'],
            ':address'                   => normaliseString($row['Address']),
            ':street_number'             => normaliseInt($row['StreetNumber']),
            ':street_name'               => normaliseString($row['StreetName']),
            ':latitude'                  => normaliseFloat($row['Latitude']),
            ':longitude'                 => normaliseFloat($row['Longitude']),
            ':is_business'               => (int) ($row['IsBusiness'] ?? 0),
            ':do_not_call'               => (int) ($row['DoNotCall'] ?? 0),
            ':do_not_call_date'          => normaliseDate($row['DoNotCallDate']),
            ':do_not_call_notes'         => normaliseString($row['DoNotCallNotes']),
            ':do_not_call_private_notes' => normaliseString($row['DoNotCallPrivateNotes']),
            ':created_at'                => $now,
            ':updated_at'                => $now,
        ];
    },
    $dryRun
);

// ── 3. HouseholderNames ────────────────────────────────────────────────────
echo "\n[3/3] HouseholderNames\n";
migrateTable(
    $reader,
    $sqlite,
    'HouseholderNames',
    'householder_names',
    'INSERT INTO householder_names
        (id, household_id, name, letter_returned, return_date, created_at, updated_at)
     VALUES
        (:id, :household_id, :name, :letter_returned, :return_date, :created_at, :updated_at)',
    function (array $row) use ($now): array {
        return [
            ':id'              => (int) $row['Id'],
            ':household_id'    => (int) $row['HouseholdId'],
            ':name'            => normaliseString($row['Name']),
            ':letter_returned' => (int) ($row['LetterReturned'] ?? 0),
            ':return_date'     => normaliseDateTime($row['ReturnDate']),
            ':created_at'      => normaliseDateTime($row['Created']) ?? $now,
            ':updated_at'      => $now,
        ];
    },
    $dryRun
);

// ── Finalise ───────────────────────────────────────────────────────────────
$sqlite->exec('PRAGMA foreign_keys = ON');

echo "\n";
if ($dryRun) {
    echo "Dry run complete — no data was written.\n";
} else {
    echo "Migration complete. SQLite row counts:\n";
    foreach (['territories', 'households', 'householder_names'] as $t) {
        $n = $sqlite->query("SELECT COUNT(*) FROM [{$t}]")->fetchColumn();
        printf("  %-25s %d\n", $t, $n);
    }
}
