<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\ChartAccountRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\ConflictException;
use ApiModel\Shared\Exceptions\NotFoundException;

final class ChartAccountService
{
    /** @var list<string> */
    private const ACCOUNT_TYPES = ['asset', 'liability', 'equity', 'income', 'expense'];

    public function __construct(private readonly ChartAccountRepositoryInterface $accounts) {}

    /** @return list<array<string, mixed>> */
    public function listAccounts(string $companyId): array
    {
        return array_map($this->mapAccount(...), $this->accounts->listByCompany($companyId));
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function createAccount(string $companyId, array $data): array
    {
        $code = trim((string) ($data['code'] ?? ''));
        $name = trim((string) ($data['name'] ?? ''));
        $accountType = strtolower(trim((string) ($data['account_type'] ?? '')));
        $isHeader = (bool) ($data['is_header'] ?? false);
        $parentId = $this->nullableId($data['parent_id'] ?? null);

        $this->validateAccountFields($code, $name, $accountType);
        $this->validateParent($companyId, $parentId);

        if ($this->accounts->findByCode($companyId, $code) !== null) {
            throw new ConflictException('Ya existe una cuenta con ese código');
        }

        $id = Database::uuid();
        $this->accounts->insert($id, $companyId, $parentId, $code, $name, $accountType, $isHeader);

        $row = $this->accounts->findById($id);

        return $this->mapAccount($row ?? []);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function updateAccount(string $companyId, string $id, array $data): array
    {
        $row = $this->accounts->findById($id);
        if ($row === null || (string) $row['company_id'] !== $companyId) {
            throw new NotFoundException('Cuenta no encontrada');
        }

        $code = trim((string) ($data['code'] ?? $row['code']));
        $name = trim((string) ($data['name'] ?? $row['name']));
        $accountType = strtolower(trim((string) ($data['account_type'] ?? $row['account_type'])));
        $isHeader = (bool) ($data['is_header'] ?? $row['is_header']);
        $isActive = (bool) ($data['is_active'] ?? $row['is_active']);
        $parentId = array_key_exists('parent_id', $data)
            ? $this->nullableId($data['parent_id'])
            : ($row['parent_id'] !== null ? (string) $row['parent_id'] : null);

        $this->validateAccountFields($code, $name, $accountType);
        if ($parentId === $id) {
            throw new BadRequestException('Una cuenta no puede ser padre de sí misma');
        }
        $this->validateParent($companyId, $parentId);

        $existing = $this->accounts->findByCode($companyId, $code);
        if ($existing !== null && (string) $existing['id'] !== $id) {
            throw new ConflictException('Ya existe otra cuenta con ese código');
        }

        if (! $isHeader && $this->accounts->hasChildren($id)) {
            throw new BadRequestException('La cuenta tiene subcuentas; debe permanecer como grupo (header)');
        }

        $this->accounts->update($id, $parentId, $code, $name, $accountType, $isHeader, $isActive);

        return $this->mapAccount($this->accounts->findById($id) ?? []);
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapAccount(array $row): array
    {
        return [
            'id' => $row['id'] ?? null,
            'company_id' => $row['company_id'] ?? null,
            'parent_id' => $row['parent_id'] ?? null,
            'code' => $row['code'] ?? '',
            'name' => $row['name'] ?? '',
            'account_type' => $row['account_type'] ?? '',
            'is_header' => (bool) ($row['is_header'] ?? false),
            'is_active' => (bool) ($row['is_active'] ?? true),
        ];
    }

    private function validateAccountFields(string $code, string $name, string $accountType): void
    {
        if ($code === '' || $name === '') {
            throw new BadRequestException('Código y nombre de cuenta son obligatorios');
        }
        if (! in_array($accountType, self::ACCOUNT_TYPES, true)) {
            throw new BadRequestException('Tipo de cuenta inválido');
        }
    }

    private function validateParent(string $companyId, ?string $parentId): void
    {
        if ($parentId === null) {
            return;
        }

        $parent = $this->accounts->findById($parentId);
        if ($parent === null || (string) $parent['company_id'] !== $companyId) {
            throw new BadRequestException('Cuenta padre inválida');
        }
    }

    private function nullableId(mixed $value): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }

        return trim((string) $value);
    }
}
