<?php
namespace Incoviba\Service;

use Exception;
use DateTimeImmutable;
use DateMalformedStringException;
use Incoviba\Exception\ServiceAction\{Read, Update};
use Psr\Log\LoggerInterface;
use Incoviba\Common\Ideal\Service;
use Incoviba\Common\Implement;
use Incoviba\Repository;
use Incoviba\Model;

class Venta extends Service
{
    public function __construct(
        LoggerInterface $logger,
        protected Repository\Venta $ventaRepository,
        protected Repository\Venta\EstadoVenta $estadoVentaRepository,
        protected Repository\Venta\TipoEstadoVenta $tipoEstadoVentaRepository,
        protected Repository\Venta\Credito $creditoRepository,
        protected Repository\Venta\Escritura $escrituraRepository,
        protected Repository\Venta\Pago $pagoRepository,
        protected Repository\Venta\EstadoPago $estadoPagoRepository,
        protected Venta\FormaPago $formaPagoService,
        protected Venta\Propietario $propietarioService,
        protected Venta\Propiedad $propiedadService,
        protected Venta\Pie $pieService,
        protected Venta\Subsidio $subsidioService,
        protected Venta\Credito $creditoService,
        protected Venta\BonoPie $bonoPieService,
        protected Venta\Pago $pagoService,
        protected Proyecto\Terreno $terrenoService,
        protected Money $moneyService,
        protected Valor $valorService
    ) {
        parent::__construct($logger);
    }

    /**
     * @param int $venta_id
     * @return Model\Venta
     * @throws Read
     */
    public function getById(int $venta_id): Model\Venta
    {
        try {
            return $this->process($this->ventaRepository->fetchById($venta_id));
        } catch (Implement\Exception\EmptyResult $exception) {
            throw new Read(__CLASS__, $exception);
        }
    }
    public function getByProyecto(int $proyecto_id): array
    {
        $ventas = $this->ventaRepository->fetchByProyecto($proyecto_id);
        return array_map([$this, 'process'], $ventas);
    }
    public function getActivaByProyecto(int $proyecto_id): array
    {
        $ventas = $this->ventaRepository->fetchActivaByProyecto($proyecto_id);
        return array_map([$this, 'process'], $ventas);
    }
    public function getIdsByProyecto(int $proyecto_id): array
    {
        return $this->ventaRepository->fetchIdsByProyecto($proyecto_id);
    }
    public function getByProyectoAndUnidad(string $proyecto_nombre, int $unidad_descripcion): Model\Venta
    {
        $venta = $this->ventaRepository->fetchByProyectoAndUnidad($proyecto_nombre, $unidad_descripcion);
        return $this->process($venta);
    }

    /**
     * @param string $unidad
     * @param string $tipo
     * @return array
     * @throws Read
     */
    public function getByUnidad(string $unidad, string $tipo): array
    {
        try {
            $ventas = $this->ventaRepository->fetchByUnidad($unidad, $tipo);
            return array_map([$this, 'process'], $ventas);
        } catch (Implement\Exception\EmptyResult $exception) {
            throw new Read(__CLASS__, $exception);
        }
    }

    /**
     * @param int $unidad_id
     * @return Model\Venta
     * @throws Read
     */
    public function getActiveByUnidadId(int $unidad_id): Model\Venta
    {
        try {
            return $this->process($this->ventaRepository->fetchActiveByUnidadId($unidad_id));
        } catch (Implement\Exception\EmptyResult $exception) {
            throw new Read(__CLASS__, $exception);
        }
    }

    /**
     * @param array $unidad_ids
     * @return array
     * @throws Read
     */
    public function getActiveByUnidadIds(array $unidad_ids): array
    {
        try {
            $ventas = $this->ventaRepository->fetchActiveArrayByUnidadIds($unidad_ids);
            return array_map(function($data) {
                return [
                    'unidad_id' => $data['unidad_id'],
                    'venta' => $this->processSimple($data['venta'])
                ];
            }, $ventas);
        } catch (Implement\Exception\EmptyResult $exception) {
            throw new Read(__CLASS__, $exception);
        }
    }
    public function getByPropietario(string $propietario): array
    {
        $ventas = $this->ventaRepository->fetchByPropietario($propietario);
        return array_map([$this, 'process'], $ventas);
    }
    public function getByPrecio(string $precio): array
    {
        $ventas = $this->ventaRepository->fetchByPrecio($precio);
        return array_map([$this, 'process'], $ventas);
    }
    public function getEscriturasByProyecto(int $proyecto_id): array
    {
        $ventas = $this->ventaRepository->fetchEscriturasByProyecto($proyecto_id);
        return array_map([$this, 'process'], $ventas);
    }
    public function getByIdForSearch(int $venta_id): array
    {
        return $this->ventaRepository->fetchByIdForSearch($venta_id);
    }
    public function getByIdForList(int $venta_id): array
    {
        return $this->ventaRepository->fetchByIdForList($venta_id);
    }
    public function getByPie(int $pie_id): Model\Venta
    {
        return $this->process($this->ventaRepository->fetchByPie($pie_id));
    }
    public function getFacturacionById(int $venta_id): array
    {
        $venta = $this->getById($venta_id);
        $escritura = (in_array($venta->currentEstado()->tipoEstadoVenta->descripcion, ['escriturando', 'firmado por inmobiliaria'])) ? $venta->currentEstado()->fecha : $venta->fecha;
        $data = [
            'id' => $venta->id,
            'fecha' => $venta->fecha->format('Y-m-d'),
            'valor' => $venta->valor,
            'escritura' => $escritura->format('Y-m-d'),
            'unidades' => [],
        ];
        foreach ($venta->propiedad()->unidades as $unidad) {
            $data['unidades'] []= [
                'id' => $unidad->id,
                'tipo' => ucwords($unidad->proyectoTipoUnidad->tipoUnidad->descripcion),
                'descripcion' => $unidad->descripcion,
                'prorrateo' => $unidad->prorrateo,
                'valor' => $unidad->valor,
                'precio' => (isset($unidad->currentPrecio)) ? $unidad->currentPrecio->valor : 0
            ];
        }
        $principal = $venta->propiedad()->principal();
        $data['principal'] = [
            'id' => $principal->id,
            'descripcion' => $principal->descripcion
        ];
        return $data;
    }

    protected function process(Model\Venta $venta): Model\Venta
    {
        $venta->addFactory('formaPago', (new Implement\Repository\Factory())
            ->setCallable([$this->formaPagoService, 'getByVenta'])
            ->setArgs(['venta_id' => $venta->id]));
        $venta->addFactory('estados', (new Implement\Repository\Factory())
            ->setCallable([$this->estadoVentaRepository, 'fetchByVenta'])
            ->setArgs([$venta->id]));
        $venta->addFactory('currentEstado', (new Implement\Repository\Factory())
            ->setCallable([$this->estadoVentaRepository, 'fetchCurrentByVenta'])
            ->setArgs([$venta->id]));
        return $venta;
    }

    /**
     * @param array $venta
     * @return array
     * @throws Read
     */
    protected function processSimple(array $venta): array
    {
        $output = $venta;
        $output['propiedad'] = $this->propiedadService->getArrayById($venta['propiedad']);
        return $output;
    }

    /**
     * @throws Exception
     */
    public function add(array $data): Model\Venta
    {
        $fecha = new DateTimeImmutable($data['fecha_venta']);
        $data['uf'] = $this->moneyService->getUF($fecha);
        $propietario = $this->addPropietario($data);
        $propiedad = $this->addPropiedad($data);
        $formaPago = $this->addFormaPago($data);
        $venta_data = [
            'propietario' => $propietario->rut,
            'propiedad' => $propiedad->id,
            'fecha' => $fecha->format('Y-m-d'),
            'valor_uf' => $this->valorService->clean($data['valor']),
            'fecha_ingreso' => (new DateTimeImmutable())->format('Y-m-d'),
            'uf' => $data['uf']
        ];
        $map = ['pie', 'subsidio', 'credito', 'bono_pie'];
        foreach ($map as $field) {
            $name = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $field))));
            if (isset($formaPago->{$name})) {
                $venta_data[$field] = $formaPago->{$name}->id;
            }
        }
        try {
            return $this->ventaRepository->fetchByPropietarioAndPropiedad($propietario->rut, $propiedad->id);
        } catch (Implement\Exception\EmptyResult) {
            $venta = $this->ventaRepository->create($venta_data);
            $venta->setFormaPago($formaPago);
            $venta = $this->ventaRepository->save($venta);

            $tipoEstado = $this->tipoEstadoVentaRepository->fetchByDescripcion('vigente');
            $estado = $this->estadoVentaRepository->create([
                'venta' => $venta->id,
                'estado' => $tipoEstado->id,
                'fecha' => $venta->fecha->format('Y-m-d')
            ]);
            $this->estadoVentaRepository->save($estado);

            return $venta;
        }
    }
    protected function addPropietario(array $data): Model\Venta\Propietario
    {
        if (isset($data['natural_uno'])) {
            if (isset($data['natural_multiple'])) {
                return $this->addDosPropietarios($data);
            }
            return $this->addUnPropietario($data);
        }
        return $this->addSociedad($data);
    }
    protected function addUnPropietario(array $data): Model\Venta\Propietario
    {
        $fields = array_fill_keys([
            'rut',
            'nombres',
            'apellido_paterno',
            'apellido_materno',
            'calle',
            'numero',
            'extra',
            'comuna'
        ], 0);
        $filtered_data = array_intersect_key($data, $fields);
        return $this->propietarioService->addPropietario($filtered_data);
    }
    protected function addDosPropietarios(array $data): Model\Venta\Propietario
    {
        $fields = array_fill_keys([
            'rut_otro',
            'nombres_otro',
            'apellido_paterno_otro',
            'apellido_materno_otro',
            'calle_otro',
            'numero_otro',
            'extra_otro',
            'comuna_otro'
        ], 0);
        $filtered_data = array_intersect_key($data, $fields);
        $mapped_data = array_combine([
            'rut',
            'nombres',
            'apellido_paterno',
            'apellido_materno',
            'calle',
            'numero',
            'extra',
            'comuna'
        ], $filtered_data);
        $otro = $this->propietarioService->addPropietario($mapped_data);

        $data['otro'] = $otro->rut;
        $fields = array_fill_keys([
            'rut',
            'nombres',
            'apellido_paterno',
            'apellido_materno',
            'calle',
            'numero',
            'extra',
            'comuna',
            'otro'
        ], 0);
        $filtered_data = array_intersect_key($data, $fields);
        return $this->propietarioService->addPropietario($filtered_data);
    }
    protected function addSociedad(array $data): Model\Venta\Propietario
    {
        $representante = $this->addUnPropietario($data);

        $data['representante'] = $representante->rut;
        $fields = array_fill_keys([
            'rut_sociedad',
            'razon_social',
            'calle_comercial',
            'numero_comercial',
            'extra_comercial',
            'comuna_comercial',
            'representante'
        ], 0);
        $filtered_data = array_intersect_key($data, $fields);
        $mapped_data = array_combine([
            'rut',
            'razon_social',
            'calle',
            'numero',
            'extra',
            'comuna',
            'representante'
        ], $filtered_data);
        return $this->propietarioService->addSociedad($mapped_data);
    }
    protected function addPropiedad(array $data): Model\Venta\Propiedad
    {
        $ids = array_filter($data, function($key) {
            return str_starts_with($key, 'unidad');
        }, ARRAY_FILTER_USE_KEY);

        return $this->propiedadService->addPropiedad($ids);
    }
    protected function addFormaPago(array $data): Model\Venta\FormaPago
    {
        return $this->formaPagoService->add($data);
    }

    /**
     * @param Model\Venta $venta
     * @param array $data
     * @return Model\Venta
     * @throws Update
     */
    public function edit(Model\Venta $venta, array $data): Model\Venta
    {
        try {
            $filteredData = $this->ventaRepository->filterData($data);
            return $this->ventaRepository->edit($venta, $filteredData);
        } catch (Implement\Exception\EmptyResult $exception) {
            throw new Update(__CLASS__, $exception);
        }
    }

    protected function addEstado(Model\Venta $venta, Model\Venta\TipoEstadoVenta $tipoEstadoVenta, array $data): void
    {
        try {
            $fecha = new DateTimeImmutable($data['fecha']);
        } catch (DateMalformedStringException) {
            $fecha = new DateTimeImmutable();
        }
        $estadoData = [
            'venta' => $venta->id,
            'estado' => $tipoEstadoVenta->id,
            'fecha' => $fecha->format('Y-m-d')
        ];
        $estado = $this->estadoVentaRepository->create($estadoData);
        $this->estadoVentaRepository->save($estado);
    }

    public function escriturar(Model\Venta $venta, array $data): bool
    {
        if (in_array($venta->currentEstado()->tipoEstadoVenta->descripcion, ['escriturando', 'firmado por inmobiliaria', 'archivado'])) {
            return true;
        }
        try {
            if ($this->validarData($data, ['fecha_reajuste', 'valor_reajuste'])) {
                $this->reajustarEscritura($venta, $data);
            }
            if ($this->validarData($data, ['fecha_pago'], ['valor_pago_pesos', 'valor_pago_ufs'])) {
                $this->abonoEscritura($venta, $data);
            }
            if ($this->validarData($data, ['banco_credito'], ['valor_credito'])) {
                $this->editCredito($venta, $data);
            }
            if ($this->validarData($data, ['valor_subsidio', 'valor_ahorro', 'fecha'])) {
                $this->subsidioEscritura($venta, $data);
            }
            $tipoEstado = $this->tipoEstadoVentaRepository->fetchByDescripcion('escriturando');
            $this->addEstado($venta, $tipoEstado, ['fecha' => $data['fecha']]);
            return true;
        } catch (Implement\Exception\EmptyResult) {
            return false;
        }
    }
    protected function validarData(array $data, array $keys, array $optionals = []): bool
    {
        foreach ($keys as $key) {
            if (!isset($data[$key])) {
                return false;
            }
            if ($data[$key] === '') {
                return false;
            }
        }
        foreach ($optionals as $key) {
            if (isset($data[$key]) and $data[$key] !== '') {
                break;
            }
        }
        return true;
    }

    protected function reajustarEscritura(Model\Venta $venta, array $data): void
    {
        try {
            $fecha = new DateTimeImmutable($data['fecha_reajuste']);
        } catch (DateMalformedStringException) {
            $fecha = new DateTimeImmutable();
        }
        $reajusteData = [
            'valor' => $this->valorService->clean($data['valor_reajuste']),
            'fecha' => $fecha->format('Y-m-d')
        ];
        $pie = $venta->formaPago()->pie;
        $this->pieService->reajustar($pie, $reajusteData);
    }

    protected function abonoEscritura(Model\Venta $venta, array $data): void
    {
        try {
            $fecha = new DateTimeImmutable($data['fecha_pago']);
        } catch (DateMalformedStringException) {
            $fecha = new DateTimeImmutable();
        }
        $uf = $this->moneyService->getUF($fecha);
        $valor = $data['valor_pago_ufs'] !== '' ? $this->valorService->clean($data['valor_pago_ufs']) * $uf : $this->valorService->clean($data['valor_pago_pesos']);
        $pagoData = [
            'valor' => $valor,
            'fecha' => $fecha->format('Y-m-d'),
            'uf' => $uf
        ];
        $pago = $this->pagoService->add($pagoData);
        $escrituraData = [
            'valor' => $valor,
            'fecha' => $fecha->format('Y-m-d'),
            'uf' => $uf,
            'pago' => $pago->id
        ];
        $escritura = $this->escrituraRepository->create($escrituraData);
        $escritura = $this->escrituraRepository->save($escritura);
        $this->ventaRepository->edit($venta, ['escritura' => $escritura->id]);
    }

    protected function subsidioEscritura(Model\Venta $venta, array $data): void
    {
        try {
            $fecha = new DateTimeImmutable($data['fecha']);
        } catch (DateMalformedStringException) {
            $fecha = new DateTimeImmutable();
        }
        $uf = $this->moneyService->getUF($fecha);
        $subsidioData = [
            'fecha_venta' => $fecha->format('Y-m-d'),
            'ahorro' => $this->valorService->clean($data['valor_ahorro']),
            'subsidio' => $this->valorService->clean($data['valor_subsidio']),
            'uf' => $uf
        ];
        $formaPago = $this->addFormaPago($subsidioData);
        $this->ventaRepository->edit($venta, ['subsidio' => $formaPago->subsidio->id]);
    }

    protected function editCredito(Model\Venta $venta, array $data): void
    {
        try {
            $fecha = new DateTimeImmutable($data['fecha']);
        } catch (DateMalformedStringException) {
            $fecha = new DateTimeImmutable();
        }
        $uf = $this->moneyService->getUF($fecha);
        $valor = $this->valorService->clean($data['valor_credito']) * $uf;
        if ($venta->formaPago()->credito === null) {
            if ($data['valor_credito'] === 0) {
                return;
            }
            $pagoData = [
                'valor' => $valor,
                'fecha' => $fecha->format('Y-m-d'),
                'uf' => $uf
            ];
            $pago = $this->pagoService->add($pagoData);
            $creditoData = [
                'banco' => $data['banco_credito'],
                'valor' => $valor,
                'pago' => $pago->id
            ];
            $credito = $this->creditoRepository->create($creditoData);
            $credito = $this->creditoRepository->save($credito);
            $this->ventaRepository->edit($venta, ['credito' => $credito->id]);
            return;
        }
        if ($data['valor_credito'] === 0) {
            $this->pagoRepository->remove($venta->formaPago()->credito->pago);
            $this->creditoRepository->remove($venta->formaPago()->credito);
            $this->ventaRepository->edit($venta, ['credito' => null]);
            return;
        }
        $this->pagoRepository->edit($venta->formaPago()->credito->pago, [
            'valor' => $valor,
            'banco' => $data['banco_credito'],
            'uf' => $uf
        ]);
        $this->estadoPagoRepository->edit($venta->formaPago()->credito->pago->currentEstado, [
            'fecha' => $fecha->format('Y-m-d')
        ]);
        $this->creditoRepository->edit($venta->formaPago()->credito, [
            'valor' => $valor,
            'fecha' => $fecha->format('Y-m-d'),
            'uf' => $uf
        ]);
    }

    public function desistir(Model\Venta $venta, array $data): bool
    {
        try {
            if ($this->validarData($data, ['fecha', 'devolucion'])) {
                $pago = $this->pagoService->add(['fecha' => $data['fecha'], 'valor' => $this->valorService->clean($data['devolucion'])]);
                $this->ventaRepository->edit($venta, ['resciliacion' => $pago->id]);
            }
            $tipoEstado = $this->tipoEstadoVentaRepository->fetchByDescripcion('desistida');
            $this->addEstado($venta, $tipoEstado, ['fecha' => $data['fecha']]);
            return true;
        } catch (Implement\Exception\EmptyResult) {
            return false;
        }
    }
    public function insistir(Model\Venta $venta): bool
    {
        try {
            $pago = $venta->resciliacion();
            $this->pagoService->delete($pago);
            $estado = $venta->currentEstado();
            $this->estadoVentaRepository->remove($estado);
            $this->ventaRepository->edit($venta, ['resciliacion' => null]);
            return true;
        } catch (Implement\Exception\EmptyResult) {
            return false;
        }
    }
}
