<?php

declare(strict_types=1);

namespace ApiModel\Service;

use ApiModel\Domain\Repository\BranchRepositoryInterface;
use ApiModel\Domain\Repository\CompanyRepositoryInterface;
use ApiModel\Domain\Repository\ProfileRepositoryInterface;
use ApiModel\Domain\Repository\UserAccessRepositoryInterface;
use ApiModel\Infrastructure\Persistence\Database;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\ConflictException;
use ApiModel\Shared\Exceptions\NotFoundException;

final class OrganizationService
{
    public function __construct(
        private readonly CompanyRepositoryInterface $companies,
        private readonly BranchRepositoryInterface $branches,
        private readonly UserAccessRepositoryInterface $userAccess,
        private readonly ProfileRepositoryInterface $profiles
    ) {}

    /** @return list<array<string, mixed>> */
    public function listCompanies(string $userId): array
    {
        if ($this->profiles->userIsAdministrator($userId)) {
            return array_map($this->mapCompany(...), $this->companies->listAll());
        }

        return array_map($this->mapCompany(...), $this->companies->listForUser($userId));
    }

    /** @return array<string, mixed> */
    public function getCompany(string $id): array
    {
        $row = $this->companies->findById($id);
        if ($row === null) {
            throw new NotFoundException('Empresa no encontrada');
        }

        return $this->mapCompany($row);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function createCompany(array $data): array
    {
        $legalName = trim((string) ($data['legal_name'] ?? ''));
        $taxId = trim((string) ($data['tax_id'] ?? ''));
        $baseCurrency = strtoupper(trim((string) ($data['base_currency'] ?? 'USD')));

        if ($legalName === '' || $taxId === '') {
            throw new BadRequestException('Razón social y NIT/RUC son obligatorios');
        }

        if ($this->companies->findByTaxId($taxId) !== null) {
            throw new ConflictException('Ya existe una empresa con ese identificador fiscal');
        }

        $id = Database::uuid();
        $this->companies->insert(
            $id,
            $legalName,
            $this->nullableString($data['trade_name'] ?? null),
            $taxId,
            $baseCurrency,
            $this->nullableString($data['email'] ?? null),
            $this->nullableString($data['phone'] ?? null),
            $this->nullableString($data['address'] ?? null)
        );

        return $this->getCompany($id);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function updateCompany(string $id, array $data): array
    {
        if ($this->companies->findById($id) === null) {
            throw new NotFoundException('Empresa no encontrada');
        }

        $legalName = trim((string) ($data['legal_name'] ?? ''));
        $taxId = trim((string) ($data['tax_id'] ?? ''));
        $baseCurrency = strtoupper(trim((string) ($data['base_currency'] ?? 'USD')));

        if ($legalName === '' || $taxId === '') {
            throw new BadRequestException('Razón social y NIT/RUC son obligatorios');
        }

        $existing = $this->companies->findByTaxId($taxId);
        if ($existing !== null && $existing['id'] !== $id) {
            throw new ConflictException('Ya existe otra empresa con ese identificador fiscal');
        }

        $this->companies->update(
            $id,
            $legalName,
            $this->nullableString($data['trade_name'] ?? null),
            $taxId,
            $baseCurrency,
            $this->nullableString($data['email'] ?? null),
            $this->nullableString($data['phone'] ?? null),
            $this->nullableString($data['address'] ?? null),
            (bool) ($data['is_active'] ?? true)
        );

        return $this->getCompany($id);
    }

    /** @return list<array<string, mixed>> */
    public function listBranches(string $companyId): array
    {
        if ($this->companies->findById($companyId) === null) {
            throw new NotFoundException('Empresa no encontrada');
        }

        return array_map(
            fn (array $row): array => $this->mapBranch($row, (string) ($this->companies->findById($companyId)['base_currency'] ?? 'USD')),
            $this->branches->listByCompany($companyId)
        );
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function createBranch(string $companyId, array $data): array
    {
        if ($this->companies->findById($companyId) === null) {
            throw new NotFoundException('Empresa no encontrada');
        }

        $name = trim((string) ($data['name'] ?? ''));
        if ($name === '') {
            throw new BadRequestException('El nombre de la sucursal es obligatorio');
        }

        $company = $this->companies->findById($companyId);
        $functionalCurrency = strtoupper(trim((string) ($company['base_currency'] ?? 'USD')));

        $id = Database::uuid();
        $this->branches->insert(
            $id,
            $companyId,
            $name,
            $this->nullableString($data['code'] ?? null),
            $this->nullableString($data['address'] ?? null),
            $this->nullableString($data['phone'] ?? null),
            (bool) ($data['is_main'] ?? false),
            $this->resolveOperatingCurrency($data['operating_currency'] ?? null, $functionalCurrency)
        );

        $branch = $this->branches->findById($id);

        return $this->mapBranch($branch ?? [], $functionalCurrency);
    }

    /** @param array<string, mixed> $data @return array<string, mixed> */
    public function updateBranch(string $id, array $data): array
    {
        $branchRow = $this->branches->findById($id);
        if ($branchRow === null) {
            throw new NotFoundException('Sucursal no encontrada');
        }

        $company = $this->companies->findById((string) ($branchRow['company_id'] ?? ''));
        $functionalCurrency = strtoupper(trim((string) ($company['base_currency'] ?? 'USD')));

        $name = trim((string) ($data['name'] ?? ''));
        if ($name === '') {
            throw new BadRequestException('El nombre de la sucursal es obligatorio');
        }

        $this->branches->update(
            $id,
            $name,
            $this->nullableString($data['code'] ?? null),
            $this->nullableString($data['address'] ?? null),
            $this->nullableString($data['phone'] ?? null),
            (bool) ($data['is_main'] ?? false),
            (bool) ($data['is_active'] ?? true),
            $this->resolveOperatingCurrency($data['operating_currency'] ?? null, $functionalCurrency)
        );

        $branch = $this->branches->findById($id);

        return $this->mapBranch($branch ?? [], $functionalCurrency);
    }

    public function deleteBranch(string $id): void
    {
        $branch = $this->branches->findById($id);
        if ($branch === null) {
            throw new NotFoundException('Sucursal no encontrada');
        }

        if ((bool) ($branch['is_main'] ?? false)) {
            throw new BadRequestException('No se puede eliminar la sucursal principal.');
        }

        $this->branches->delete($id);
    }


    /** @return array<string, mixed> */
    public function getMyContext(string $userId): array
    {
        $companyRows = $this->companies->listForUser($userId);

        $accessRows = $this->userAccess->listForUser($userId);
        $accessByCompany = [];
        foreach ($accessRows as $access) {
            $accessByCompany[$access['company_id']] = $access;
        }

        $companies = [];
        $defaultCompanyId = null;
        $defaultBranchId = null;

        foreach ($companyRows as $row) {
            if (! (bool) ($row['is_active'] ?? true)) {
                continue;
            }

            $companyId = (string) $row['id'];
            $functionalCurrency = strtoupper(trim((string) ($row['base_currency'] ?? 'USD')));
            $branchList = array_map(
                fn (array $branchRow): array => $this->mapBranch($branchRow, $functionalCurrency),
                $this->branches->listByCompany($companyId)
            );
            $activeBranches = array_values(array_filter($branchList, static fn (array $b): bool => (bool) $b['is_active']));

            $companies[] = [
                ...$this->mapCompany($row),
                'branches' => $activeBranches,
            ];

            $access = $accessByCompany[$companyId] ?? null;
            if ($access !== null && (bool) ($access['is_default'] ?? false)) {
                $defaultCompanyId = $companyId;
                $defaultBranchId = $access['branch_id'] !== null ? (string) $access['branch_id'] : null;
            }
        }

        if ($defaultCompanyId === null && $companies !== []) {
            $defaultCompanyId = $companies[0]['id'];
            $defaultBranchId = $companies[0]['branches'][0]['id'] ?? null;
        }

        $defaultFunctionalCurrency = null;
        $defaultOperatingCurrency = null;
        foreach ($companies as $company) {
            if ($company['id'] === $defaultCompanyId) {
                $defaultFunctionalCurrency = $company['functional_currency'] ?? $company['base_currency'] ?? 'USD';
                foreach ($company['branches'] as $branch) {
                    if ($branch['id'] === $defaultBranchId) {
                        $defaultOperatingCurrency = $branch['operating_currency'] ?? $defaultFunctionalCurrency;
                        break;
                    }
                }
                if ($defaultOperatingCurrency === null && $company['branches'] !== []) {
                    $defaultOperatingCurrency = $company['branches'][0]['operating_currency'] ?? $defaultFunctionalCurrency;
                }
                break;
            }
        }

        return [
            'companies' => $companies,
            'default_company_id' => $defaultCompanyId,
            'default_branch_id' => $defaultBranchId,
            'default_functional_currency' => $defaultFunctionalCurrency,
            'default_operating_currency' => $defaultOperatingCurrency,
        ];
    }

    public function userHasAccess(string $userId, string $companyId, ?string $branchId = null): bool
    {
        if (! $this->userAccess->userHasCompanyAccess($userId, $companyId)) {
            return false;
        }

        if ($branchId === null || $branchId === '') {
            return true;
        }

        return $this->userAccess->branchBelongsToCompany($branchId, $companyId);
    }

    /** @return list<array<string, mixed>> */
    public function getUserAccessList(string $userId): array
    {
        $allCompanies = $this->companies->listAll();
        $accessRows = $this->userAccess->listForUser($userId);

        $accessMap = [];
        foreach ($accessRows as $row) {
            $accessMap[(string) $row['company_id']] = $row;
        }

        $result = [];
        foreach ($allCompanies as $comp) {
            $compId = (string) $comp['id'];
            $hasAccess = isset($accessMap[$compId]);
            $result[] = [
                'company_id' => $compId,
                'legal_name' => $comp['legal_name'],
                'trade_name' => $comp['trade_name'] ?? '',
                'tax_id' => $comp['tax_id'],
                'activo' => $hasAccess,
                'can_manage' => $hasAccess ? (bool) ($accessMap[$compId]['can_manage'] ?? false) : false,
                'is_default' => $hasAccess ? (bool) ($accessMap[$compId]['is_default'] ?? false) : false,
            ];
        }

        return $result;
    }

    /** @param list<array<string, mixed>> $accessList */
    public function setUserAccess(string $userId, array $accessList): void
    {
        $normalized = [];
        $hasDefault = false;

        foreach ($accessList as $item) {
            $companyId = trim((string) ($item['company_id'] ?? ''));
            if ($companyId === '' || $this->companies->findById($companyId) === null) {
                throw new BadRequestException('Empresa inválida en accesos de usuario');
            }

            $branchId = isset($item['branch_id']) ? trim((string) $item['branch_id']) : null;
            if ($branchId !== null && $branchId !== '' && ! $this->userAccess->branchBelongsToCompany($branchId, $companyId)) {
                throw new BadRequestException('Sucursal no pertenece a la empresa indicada');
            }

            $isDefault = (bool) ($item['is_default'] ?? false);
            if ($isDefault) {
                $hasDefault = true;
            }

            $normalized[] = [
                'company_id' => $companyId,
                'branch_id' => $branchId !== '' ? $branchId : null,
                'can_manage' => (bool) ($item['can_manage'] ?? false),
                'is_default' => $isDefault,
            ];
        }

        if ($normalized !== [] && ! $hasDefault) {
            $normalized[0]['is_default'] = true;
        }

        $this->userAccess->replaceForUser($userId, $normalized);
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapCompany(array $row): array
    {
        $functionalCurrency = strtoupper(trim((string) ($row['base_currency'] ?? 'USD')));

        return [
            'id' => $row['id'],
            'legal_name' => $row['legal_name'],
            'trade_name' => $row['trade_name'],
            'tax_id' => $row['tax_id'],
            'base_currency' => $functionalCurrency,
            'functional_currency' => $functionalCurrency,
            'email' => $row['email'],
            'phone' => $row['phone'],
            'address' => $row['address'],
            'is_active' => (bool) ($row['is_active'] ?? true),
        ];
    }

    /** @param array<string, mixed> $row @return array<string, mixed> */
    private function mapBranch(array $row, ?string $functionalCurrency = null): array
    {
        $functional = $functionalCurrency !== null
            ? strtoupper(trim($functionalCurrency))
            : null;
        $operating = strtoupper(trim((string) ($row['operating_currency'] ?? '')));
        if ($operating === '' || strlen($operating) !== 3) {
            $operating = $functional ?? 'USD';
        }

        return [
            'id' => $row['id'] ?? null,
            'company_id' => $row['company_id'] ?? null,
            'name' => $row['name'] ?? '',
            'code' => $row['code'] ?? null,
            'address' => $row['address'] ?? null,
            'phone' => $row['phone'] ?? null,
            'is_main' => (bool) ($row['is_main'] ?? false),
            'is_active' => (bool) ($row['is_active'] ?? true),
            'operating_currency' => $operating,
            'functional_currency' => $functional,
        ];
    }

    private function resolveOperatingCurrency(?string $operatingCurrency, string $functionalCurrency): string
    {
        $operating = strtoupper(trim((string) ($operatingCurrency ?? '')));
        if ($operating === '') {
            return strtoupper(trim($functionalCurrency));
        }

        if (! preg_match('/^[A-Z]{3}$/', $operating)) {
            throw new BadRequestException('La moneda operativa debe ser un código ISO de 3 letras');
        }

        return $operating;
    }

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

        $trimmed = trim((string) $value);

        return $trimmed === '' ? null : $trimmed;
    }
}
