<?php

declare(strict_types=1);

namespace apexl\Io\modules\product\Service;

use apexl\Io\collections\LoaderConditionCollection;
use apexl\Io\dto\LoaderCondition;
use apexl\Io\modules\product\Entity\Product;
use apexl\Io\modules\user\services\CurrentUserFactory;

/**
 * CRUD + lookups for catalogue products (V3). Prices/costs are integer
 * minor units. `code` is unique.
 */
final readonly class ProductService
{
    public const array KINDS = ['service', 'material', 'expense', 'bundle'];

    public function __construct(private CurrentUserFactory $currentUserFactory) {}

    /** @return list<Product> ordered by sort_order then code. */
    public function list(): array
    {
        $rows = Product::findMultiple(limit: null)->toArray();
        usort($rows, static fn(Product $a, Product $b): int =>
            [(int) $a->sort_order, (string) $a->code] <=> [(int) $b->sort_order, (string) $b->code]);
        return array_values($rows);
    }

    /** @return list<Product> */
    public function listActive(): array
    {
        return array_values(array_filter($this->list(), static fn(Product $p): bool => (int) $p->is_active === 1));
    }

    public function find(int $id): ?Product
    {
        return $id > 0 ? Product::find($id) : null;
    }

    public function findByCode(string $code): ?Product
    {
        $code = trim($code);
        if ($code === '') return null;
        $cond = new LoaderConditionCollection();
        $cond->add(LoaderCondition::equals('code', $code));
        return Product::findMultiple(conditions: $cond, limit: 1)->pop();
    }

    /** @param array<string,mixed> $data */
    public function create(array $data): Product
    {
        $this->validate($data, null);
        $now = time();
        $p = new Product();
        $this->apply($p, $data);
        $p->created     = $now;
        $p->created_by  = $this->userId();
        $p->modified    = $now;
        $p->modified_by = $this->userId();
        $p->store();
        return $p;
    }

    /** @param array<string,mixed> $data */
    public function update(Product $p, array $data): Product
    {
        $this->validate($data, (int) $p->id);
        $this->apply($p, $data);
        $p->modified    = time();
        $p->modified_by = $this->userId();
        $p->store();
        return $p;
    }

    public function delete(Product $p): void
    {
        $p->delete();
    }

    /** @return array<string,mixed> */
    public function serialise(Product $p): array
    {
        return [
            'id'                 => (int) $p->id,
            'code'               => (string) $p->code,
            'name'               => (string) $p->name,
            'description'        => $p->description !== null ? (string) $p->description : null,
            'kind'               => (string) $p->kind,
            'unit_price'         => (int) $p->unit_price,
            'unit_cost'          => $p->unit_cost !== null ? (int) $p->unit_cost : null,
            'currency'           => (string) $p->currency,
            'default_markup_pct' => $p->markupPercent(),
            'is_sold'            => (int) $p->is_sold === 1,
            'is_purchased'       => (int) $p->is_purchased === 1,
            'is_active'          => (int) $p->is_active === 1,
            'parent_id'          => $p->parent_id !== null ? (int) $p->parent_id : null,
            'sort_order'         => (int) $p->sort_order,
        ];
    }

    /** @param array<string,mixed> $data */
    private function apply(Product $p, array $data): void
    {
        $p->code        = trim((string) $data['code']);
        $p->name        = trim((string) $data['name']);
        $p->description = isset($data['description']) && trim((string) $data['description']) !== '' ? trim((string) $data['description']) : null;
        $p->kind        = in_array($data['kind'] ?? '', self::KINDS, true) ? (string) $data['kind'] : 'service';
        $p->unit_price  = (int) ($data['unit_price'] ?? 0);
        $p->unit_cost   = isset($data['unit_cost']) && $data['unit_cost'] !== '' && $data['unit_cost'] !== null ? (int) $data['unit_cost'] : null;
        $p->currency    = strtoupper(trim((string) ($data['currency'] ?? 'GBP'))) ?: 'GBP';
        $p->default_markup_pct = isset($data['default_markup_pct']) && $data['default_markup_pct'] !== '' && $data['default_markup_pct'] !== null
            ? number_format((float) $data['default_markup_pct'], 2, '.', '') : null;
        $p->is_sold      = !array_key_exists('is_sold', $data) || (bool) filter_var($data['is_sold'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
        $p->is_purchased = array_key_exists('is_purchased', $data) && (bool) filter_var($data['is_purchased'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
        $p->is_active    = !array_key_exists('is_active', $data) || (bool) filter_var($data['is_active'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
        $p->parent_id    = isset($data['parent_id']) && (int) $data['parent_id'] > 0 ? (int) $data['parent_id'] : null;
        $p->sort_order   = (int) ($data['sort_order'] ?? $p->sort_order ?? 0);
    }

    /** @param array<string,mixed> $data */
    private function validate(array $data, ?int $selfId): void
    {
        $code = trim((string) ($data['code'] ?? ''));
        $name = trim((string) ($data['name'] ?? ''));
        if ($code === '') throw new \InvalidArgumentException('Code is required.');
        if ($name === '') throw new \InvalidArgumentException('Name is required.');

        $existing = $this->findByCode($code);
        if ($existing !== null && (int) $existing->id !== (int) $selfId) {
            throw new \InvalidArgumentException("A product with code '{$code}' already exists.");
        }
    }

    private function userId(): ?int
    {
        $id = (int) ($this->currentUserFactory->get()?->id ?? 0);
        return $id > 0 ? $id : null;
    }
}
