<?php

declare(strict_types=1);

final class InventoryService
{
    public static function products(): array
    {
        return DB::pdo()
            ->query(
                'SELECT p.*,COALESCE(SUM(b.quantity),0) stock,(SELECT MIN(expires_on) FROM inventory_batches x WHERE x.product_id=p.id AND x.quantity>0 AND x.expires_on IS NOT NULL) next_expiry FROM inventory_products p LEFT JOIN inventory_batches b ON b.product_id=p.id GROUP BY p.id ORDER BY p.category,p.name COLLATE NOCASE',
            )
            ->fetchAll(PDO::FETCH_ASSOC);
    }
    public static function batches(): array
    {
        return DB::pdo()
            ->query(
                'SELECT b.*,p.name product_name,p.unit,p.category,dc.name supplier_name FROM inventory_batches b JOIN inventory_products p ON p.id=b.product_id LEFT JOIN directory_contacts dc ON dc.id=b.supplier_contact_id ORDER BY b.quantity>0 DESC,date(b.expires_on),p.name COLLATE NOCASE',
            )
            ->fetchAll(PDO::FETCH_ASSOC);
    }
    public static function movements(int $limit = 150): array
    {
        $limit = max(1, min(500, $limit));
        return DB::pdo()
            ->query(
                "SELECT m.*,p.name product_name,p.unit,b.batch_number,a.name animal_name,COALESCE(u.display_name,u.username) actor FROM inventory_movements m JOIN inventory_products p ON p.id=m.product_id LEFT JOIN inventory_batches b ON b.id=m.batch_id LEFT JOIN animals a ON a.id=m.animal_id LEFT JOIN users u ON u.id=m.created_by ORDER BY datetime(m.occurred_at) DESC,m.id DESC LIMIT $limit",
            )
            ->fetchAll(PDO::FETCH_ASSOC);
    }
    public static function saveProduct(array $data): int
    {
        $category = (string) ($data['category'] ?? '');
        $name = trim((string) ($data['name'] ?? ''));
        $unit = trim((string) ($data['unit'] ?? ''));
        if (!in_array($category, ['medication', 'dewormer', 'vaccine'], true) || $name === '' || $unit === '') {
            throw new RuntimeException(t('inventory.invalid_product'));
        }
        $reference = (int) ($data['reference_id'] ?? 0) ?: null;
        $db = DB::pdo();
        if (!$reference) {
            $table = match ($category) {
                'medication' => 'ref_medications',
                'dewormer' => 'ref_dewormers',
                'vaccine' => 'ref_vaccines',
            };
            $lookup = $db->prepare("SELECT id FROM $table WHERE name=? COLLATE NOCASE LIMIT 1");
            $lookup->execute([$name]);
            $reference = (int) $lookup->fetchColumn() ?: null;
        }
        $minimum = max(0, (float) str_replace(',', '.', (string) ($data['minimum_quantity'] ?? 0)));
        $s = $db->prepare(
            'INSERT INTO inventory_products(category,reference_id,name,unit,minimum_quantity) VALUES(?,?,?,?,?)',
        );
        $s->execute([$category, $reference, mb_substr($name, 0, 150), mb_substr($unit, 0, 40), $minimum]);
        return (int) $db->lastInsertId();
    }
    public static function receive(array $data): int
    {
        $product = (int) ($data['product_id'] ?? 0);
        $quantity = (float) str_replace(',', '.', (string) ($data['quantity'] ?? 0));
        if (!$product || $quantity <= 0) {
            throw new RuntimeException(t('inventory.invalid_quantity'));
        }
        $expiry = trim((string) ($data['expires_on'] ?? ''));
        if ($expiry !== '' && !self::date($expiry)) {
            throw new RuntimeException(t('inventory.invalid_date'));
        }
        $cost = trim((string) ($data['unit_cost'] ?? ''));
        $cents = $cost === '' ? null : (int) round((float) str_replace(',', '.', $cost) * 100);
        $db = DB::pdo();
        $db->beginTransaction();
        try {
            $s = $db->prepare(
                'INSERT INTO inventory_batches(product_id,batch_number,expires_on,supplier_contact_id,unit_cost_cents,quantity,notes) VALUES(?,?,?,?,?,?,?)',
            );
            $s->execute([
                $product,
                trim((string) ($data['batch_number'] ?? '')) ?: null,
                $expiry ?: null,
                (int) ($data['supplier_contact_id'] ?? 0) ?: null,
                $cents,
                $quantity,
                trim((string) ($data['notes'] ?? '')) ?: null,
            ]);
            $batch = (int) $db->lastInsertId();
            $db->prepare(
                "INSERT INTO inventory_movements(product_id,batch_id,movement_type,quantity_delta,reason,created_by) VALUES(?,?,'entry',?,?,?)",
            )->execute([$product, $batch, $quantity, t('inventory.receipt'), Auth::id()]);
            $db->commit();
            return $batch;
        } catch (Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }
    public static function move(array $data): void
    {
        $batch = (int) ($data['batch_id'] ?? 0);
        $type = (string) ($data['movement_type'] ?? '');
        $quantity = (float) str_replace(',', '.', (string) ($data['quantity'] ?? 0));
        if (!$batch || !in_array($type, ['administration', 'loss', 'correction'], true) || $quantity <= 0) {
            throw new RuntimeException(t('inventory.invalid_movement'));
        }
        $db = DB::pdo();
        $s = $db->prepare(
            'SELECT b.*,p.id product_id FROM inventory_batches b JOIN inventory_products p ON p.id=b.product_id WHERE b.id=?',
        );
        $s->execute([$batch]);
        $row = $s->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            throw new RuntimeException(t('inventory.batch_not_found'));
        }
        $delta =
            $type === 'correction'
                ? (float) str_replace(',', '.', (string) ($data['signed_quantity'] ?? 0))
                : -$quantity;
        if ($delta == 0 || (float) $row['quantity'] + $delta < 0) {
            throw new RuntimeException(t('inventory.insufficient_stock'));
        }
        $db->beginTransaction();
        try {
            $db->prepare('UPDATE inventory_batches SET quantity=quantity+? WHERE id=?')->execute([$delta, $batch]);
            $db->prepare(
                'INSERT INTO inventory_movements(product_id,batch_id,movement_type,quantity_delta,animal_id,reason,created_by) VALUES(?,?,?,?,?,?,?)',
            )->execute([
                (int) $row['product_id'],
                $batch,
                $type,
                $delta,
                (int) ($data['animal_id'] ?? 0) ?: null,
                trim((string) ($data['reason'] ?? '')) ?: null,
                Auth::id(),
            ]);
            $db->commit();
        } catch (Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }
    public static function alerts(): array
    {
        $low = [];
        $expired = [];
        foreach (self::products() as $p) {
            if ((float) $p['stock'] <= (float) $p['minimum_quantity']) {
                $low[] = $p;
            }
        }
        foreach (self::batches() as $b) {
            if (
                (float) $b['quantity'] > 0 &&
                $b['expires_on'] &&
                $b['expires_on'] <= date('Y-m-d', strtotime('+30 days'))
            ) {
                $expired[] = $b;
            }
        }
        return compact('low', 'expired');
    }
    public static function consumeBatch(int $batchId, float $quantity, ?int $animalId, string $reason): void
    {
        if ($batchId <= 0 || $quantity <= 0) {
            return;
        }
        $db = DB::pdo();
        $s = $db->prepare(
            'SELECT b.product_id,b.quantity,b.unit_cost_cents,b.supplier_contact_id,p.name FROM inventory_batches b JOIN inventory_products p ON p.id=b.product_id WHERE b.id=?',
        );
        $s->execute([$batchId]);
        $batch = $s->fetch(PDO::FETCH_ASSOC);
        if (!$batch) {
            throw new RuntimeException(t('inventory.batch_not_found'));
        }
        if ((float) $batch['quantity'] < $quantity) {
            throw new RuntimeException(t('inventory.insufficient_stock'));
        }
        $u = $db->prepare('UPDATE inventory_batches SET quantity=quantity-? WHERE id=? AND quantity>=?');
        $u->execute([$quantity, $batchId, $quantity]);
        if ($u->rowCount() !== 1) {
            throw new RuntimeException(t('inventory.insufficient_stock'));
        }
        $movement = $db->prepare(
            "INSERT INTO inventory_movements(product_id,batch_id,movement_type,quantity_delta,animal_id,reason,created_by) VALUES(?,?,'administration',?,?,?,?)",
        );
        $movement->execute([(int) $batch['product_id'], $batchId, -$quantity, $animalId, $reason, Auth::id()]);
        $movementId = (int) $db->lastInsertId();
        if ($animalId && !is_null($batch['unit_cost_cents'])) {
            $unit = (int) $batch['unit_cost_cents'];
            $db->prepare(
                "INSERT INTO animal_expenses(animal_id,clinic_contact_id,source_type,source_id,label,occurred_on,quantity,catalog_unit_cents,discount_percent,total_cents,notes,created_by) VALUES(?,?, 'inventory',?,?,date('now'),?,?,0,?,?,?)",
            )->execute([
                $animalId,
                (int) $batch['supplier_contact_id'] ?: null,
                $movementId,
                (string) $batch['name'],
                $quantity,
                $unit,
                (int) round($unit * $quantity),
                $reason,
                Auth::id(),
            ]);
        }
    }
    private static function date(string $v): bool
    {
        $d = DateTimeImmutable::createFromFormat('!Y-m-d', $v);
        return $d && $d->format('Y-m-d') === $v;
    }
}
