<?php

namespace Xentral\Modules\Voucher\Gateway;

use Xentral\Components\Database\Database;
use Xentral\Modules\Voucher\Exception\InvalidArgumentException;
use Xentral\Modules\Voucher\Exception\RuntimeException;
use DateInterval;
use DateTime;
use Exception;
use ApplicationCore;

final class VoucherGateway
{
    /** @var Database $db */
    private $db;

    /** @var ApplicationCore $app */
    private $app;

    /** @var array $validtypes */
    private $validtypes;

    /** @var array $digits */
    private $digits;

    /** @var array alphas */
    private $alphas;

    /**
     * @param Database        $database
     * @param ApplicationCore $app
     */
    public function __construct(Database $database, ApplicationCore $app)
    {
        $this->db = $database;
        $this->app = $app;
        $this->validtypes = [
            ''              => 'unbegrenzt',
            'ThisYear'      => 'Aktuelles Jahr',
            'ThisYearPlus1' => 'Aktuelles Jahr + 1',
            'ThisYearPlus2' => 'Aktuelles Jahr + 2',
            'ThisYearPlus3' => 'Aktuelles Jahr + 3',
            'Months3'       => '3 Monate',
            'Months6'       => '6 Monate',
            'Year1'         => '1 Jahr',
            'Year2'         => '2 Jahre',
            'Year3'         => '3 Jahre',
        ];
        for ($i = 0; $i < 10; $i++) {
            $this->digits[] = (String)$i;
        }
        $a = ord('A');
        for ($i = 0; $i < 26; $i++) {
            $chr = chr($a + $i);
            $this->alphas[] = $chr;
        }
    }

    /**
     * @param int   $voucherId
     * @param float $amount
     *
     * @return bool
     */
    public function changeVoucherAmount($voucherId, $amount)
    {
        $voucher = $this->getById($voucherId);
        if (empty($voucher)) {
            return false;
        }
        $possibleToChange = $this->db->fetchCol(
                sprintf(
                    'SELECT v.id FROM voucher AS v
                LEFT JOIN rechnung AS re ON v.invoice_id = re.id
                WHERE v.id = %d AND (v.invoice_id = 0 OR re.status <> \'versendet\')',
                    $voucherId
                )
            ) > 0;
        if (!$possibleToChange) {
            return false;
        }
        $this->db->perform(
            'UPDATE voucher 
            SET voucher_residual_value = :amount, voucher_original_value = :amount 
            WHERE id = :id ',
            ['amount' => $amount, 'id' => $voucherId]
        );

        return true;
    }

    /**
     * @param $voucherId
     *
     * @return array
     */
    public function getById($voucherId)
    {
        $sql = 'SELECT v.* 
                  FROM voucher AS v 
                  WHERE v.id = :voucher_id
                  LIMIT 1';
        $result = $this->db->fetchRow($sql, [
            'voucher_id' => $voucherId,
        ]);
        if (empty($result)) {
            throw new RuntimeException(sprintf('%s Vaucher not found.', $voucherId));
        }
        if ($result['tax_name'] !== 'ermaessigt' && $result['tax_name'] !== 'normal') {
            $result['tax_name'] = 'befreit';
        }

        return $result;
    }

    /**
     * @param string $voucherCode
     *
     * @return array
     */
    public function getVoucherByCode($voucherCode)
    {
        $sql = 'SELECT v.*
              FROM voucher AS v
              WHERE v.voucher_code = :voucher_code';


        $vouchers = $this->db->fetchAll($sql, [
            'voucher_code' => $voucherCode,
        ]);
        if (!empty($vouchers)) {
            foreach ($vouchers as $key => $voucher) {
                if ($voucher['tax_name'] !== 'normal' && $voucher['tax_name'] !== 'ermaessigt') {
                    $vouchers[$key]['tax_name'] = 'befreit';
                }
            }
        }

        return $vouchers;
    }


    /**
     * @param string $voucherCode
     *
     * @return bool
     */
    public function isValidVoucher($voucherCode)
    {
        if (empty($voucherCode)) {
            return false;
        }
        $sql = 'SELECT v.id 
                  FROM voucher AS v 
                  LEFT JOIN rechnung AS re ON v.invoice_id = re.id
                  WHERE v.voucher_code = :voucher_code
                  AND (IFNULL(valid_from,\'0000-00-00\') = \'0000-00-00\' OR IFNULL(valid_from,\'0000-00-00\') <= CURDATE())
                  AND (IFNULL(valid_to,\'0000-00-00\') = \'0000-00-00\' OR IFNULL(valid_to,\'0000-00-00\') >= CURDATE())
                  AND (v.invoice_id = 0 OR re.status = \'versendet\' OR re.status = \'freigegeben\')
                  LIMIT 1';
        $result = $this->db->fetchAll($sql, [
            'voucher_code' => $voucherCode,
        ]);

        return !empty($result);
    }

    /**
     * @param array|int $voucher
     * @param array     $positions
     *
     * @return bool
     */
    public function isPossibleToUseVoucher($voucher, $positions)
    {
        if (empty($voucher)) {
            return false;
        }
        if (is_int($voucher)) {
            $voucher = $this->getById($voucher);
        }
        if (!$this->isValidVoucher($voucher['voucher_code'])) {
            return false;
        }
        if (empty($positions) || !is_array($positions)) {
            return true;
        }
        $taxName = !empty($voucher['tax_name']) ? $voucher['tax_name'] : 'normal';
        if ($taxName === 'befreit') {
            return true;
        }
        $return = false;
        foreach ($positions as $position) {
            if ((int)$position['id'] === (int)$voucher['article_id']) {
                return false;
            }
            if ($taxName === !empty($position['umsatzsteuer']) ? $position['umsatzsteuer'] : 'normal') {
                $return = true;
            }
        }

        return $return;
    }


    /**
     * @refactor Order-Module
     *
     * @param int    $docId
     * @param string $doctype
     *
     * @return array Order
     */
    public function getDocumentById($docId, $doctype = 'auftrag')
    {
        if ($doctype === 'rechnung') {
            $sql = 'SELECT inv.* 
                  FROM `rechnung` AS inv 
                  WHERE inv.id = :id
                  LIMIT 1';
        } else {
            $sql = 'SELECT o.* 
                  FROM auftrag AS o 
                  WHERE o.id = :id
                  LIMIT 1';
        }
        $result = $this->db->fetchRow($sql, [
            'id' => $docId,
        ]);
        if (empty($result)) {
            throw new InvalidArgumentException(
                sprintf(
                    '%s %s does not exists.',
                    $doctype === 'rechnung' ? 'Invoice' : 'Order', $docId
                )
            );
        }

        return $result;
    }

    /**
     * @param int $orderId
     * @param int $voucherId
     *
     * @return int OrderPositionId
     */
    public function getVoucherPositionIdFromOrder($orderId, $voucherId)
    {
        $voucher = $this->getById($voucherId);
        $articleId = $voucher['article_id'];
        $sql = 'SELECT ap.id 
        FROM auftrag_position AS ap 
        WHERE ap.auftrag = :auftrag AND ap.artikel = :article_id LIMIT 1';
        $result = $this->db->fetchValue($sql, [
            'auftrag'    => $orderId,
            'article_id' => $articleId,
        ]);
        if (empty($result)) {
            throw new InvalidArgumentException(sprintf(
                'OrderPosition From Order %s not found.', $orderId
            ));
        }

        return $result;
    }

    /**
     * @param int $voucherOrderId
     *
     * @return array
     */
    public function getVoucherOrderById($voucherOrderId)
    {
        $sql = 'SELECT vo.* 
                  FROM voucher_order AS vo 
                  WHERE vo.id = :id
                  LIMIT 1';
        $result = $this->db->fetchRow($sql, [
            'id' => $voucherOrderId,
        ]);
        if (empty($result)) {
            throw new InvalidArgumentException(sprintf(
                'VocherOrder %s does not exists.', $voucherOrderId
            ));
        }

        return $result;
    }

    /**
     * @param int    $doctypeId
     * @param string $doctype
     *
     * @return array
     */
    public function getVoucherOrderByOrderId($doctypeId, $doctype = 'auftrag', $doctypePositionId = 0)
    {
        if($doctypePositionId > 0){
          $sql = 'SELECT vo.* 
                  FROM voucher_order AS vo 
                  WHERE vo.order_id = :order_id AND vo.doctype = :doctype AND vo.order_position_id = :order_position_id
                  LIMIT 1';
          $result = $this->db->fetchRow($sql, [
            'order_id' => $doctypeId,
            'doctype'  => $doctype,
            'order_position_id' => $doctypePositionId
          ]);
        }else{
          $sql = 'SELECT vo.* 
                  FROM voucher_order AS vo 
                  WHERE vo.order_id = :order_id AND vo.doctype = :doctype
                  LIMIT 1';
          $result = $this->db->fetchRow($sql, [
            'order_id' => $doctypeId,
            'doctype'  => $doctype,
          ]);
        }


        if (empty($result)) {
            throw new InvalidArgumentException(sprintf(
                'VocherOrder %s does not exists.', $doctypeId
            ));
        }

        return $result;
    }

    /**
     * @param int $voucherId
     *
     * @return float|null
     */
    public function getResidualValueFromVoucherId($voucherId)
    {
        $sql = 'SELECT v.voucher_original_value - IFNULL(SUM(ABS(vo.voucher_value)),0)
        FROM voucher AS v
        LEFT JOIN voucher_order AS vo ON v.id = vo.voucher_id
        WHERE v.id = :id
        ';

        return $this->db->fetchValue($sql, ['id' => $voucherId]);
    }

    /**
     * @param int $invoiceId
     *
     * @return array
     */
    public function getInvoiceById($invoiceId)
    {
        $sql = 'SELECT inv.* 
                  FROM rechnung AS inv 
                  WHERE inv.id = :id
                  LIMIT 1';
        $result = $this->db->fetchRow($sql, [
            'id' => $invoiceId,
        ]);
        if (empty($result)) {
            throw new InvalidArgumentException(sprintf(
                'Inovice %s does not exists.', $invoiceId
            ));
        }

        return $result;
    }

    /**
     * @param int $voucherId
     * @param int $limit
     *
     * @return array
     */
    public function getVoucherOrdersFromVoucher($voucherId, $limit = 500)
    {
        $sql = 'SELECT vo.* 
                  FROM voucher_order AS vo 
                  WHERE vo.voucher_id = :id
                  LIMIT :limit';

        return $this->db->fetchRow($sql, [
            'id'    => (int)$voucherId,
            'limit' => (int)$limit,
        ]);
    }

    /**
     * @param int|array $voucher
     * @param array     $positions
     *
     * @return float|int|mixed
     */
    public function getUsableVoucherValueByPos($voucher, $positions)
    {
        if (empty($voucher)) {
            throw new RuntimeException('No Voucher given');
        }
        if (is_int($voucher)) {
            $voucher = $this->getById($voucher);
        }
        if (!$this->isValidVoucher($voucher['voucher_code'])) {
            throw new RuntimeException(sprintf('Voucher %s is not valid', $voucher['voucher_code']));
        }
        if (empty($voucher)) {
            throw new RuntimeException('Voucher not found');
        }
        if (empty($positions) || !is_array($positions)) {
            return 0;
        }
        if ($voucher['tax_name'] !== 'normal' && $voucher['tax_name'] !== 'ermaessigt') {
            $voucher['tax_name'] = 'befreit';
        }
        $return = 0;
        $taxName = !empty($voucher['tax_name']) ? $voucher['tax_name'] : 'befreit';
        foreach ($positions as $position) {
            if ($taxName === 'befreit' || $taxName === '' || $taxName === (!empty($position['umsatzsteuer']) ? $position['umsatzsteuer'] : 'normal')) {
                $return += (float)(!empty($position['preis_brutto']) ? $position['preis_brutto'] : $position['preis']) * $position['menge'] * (1 - (!empty($position['rabatt']) ? $position['rabatt'] : 0) / 100);
            }
        }

        return $return > $voucher['voucher_residual_value'] ? $voucher['voucher_residual_value'] : $return;
    }

    /**
     * @param array $positions
     *
     * @return string
     */
    public function getVoucherTaxByPosPositions($positions)
    {
        $tax = '';
        if (!empty($positions)) {
            foreach ($positions as $position) {
                if ($position['preis_netto'] <= 0) {
                    continue;
                }
                if ($tax === '') {
                    $tax = round($position['tax'], 2);
                } elseif ($tax !== round($position['tax'], 2)) {
                    return 'befreit';
                }
            }
        }


        return empty($tax) ? 'befreit' : $tax;
    }

    /**
     * @param int|array $voucherId
     * @param int       $doctypeId
     * @param string    $doctype
     * @param float     $voucher_value
     * @param int       $positionId
     *
     * @return int voucher_order_id
     */
    public function addVoucherToOrder($voucherId, $doctypeId, $doctype, $voucher_value, $positionId = 0)
    {
        if (is_array($voucherId)) {
            $voucher = $voucherId;
            $voucherId = $voucher['id'];
        }

        if ($doctype !== 'rechnung') {
            $doctype = 'auftrag';
        }

        if (!empty($positionId)) {
            if ($doctype === 'auftrag') {
                $positionId = (int)$this->db->fetchValue(
                    'SELECT id 
                    FROM `auftrag_position` 
                    WHERE id = :position_id AND `auftrag` = :doctype_id 
                    LIMIT 1',
                    ['position_id' => $positionId, 'doctype_id' => $doctypeId]
                );
            } else {
                $positionId = (int)$this->db->fetchValue(
                    'SELECT id 
                    FROM `rechnung_position` 
                    WHERE id = :position_id AND `rechnung` = :doctype_id 
                    LIMIT 1',
                    ['position_id' => $positionId, 'doctype_id' => $doctypeId]
                );
            }
        }

        if ((float)$voucher_value === 0.0) {
            throw new InvalidArgumentException('Voucher Value must not be 0.');
        }

        if ($doctypeId <= 0) {
            throw new InvalidArgumentException(sprintf(
                '%s is not a valid %s.', $doctypeId, ($doctype === 'auftrag' ? 'OrderId' : 'InvoiceId')
            ));
        }
        if ($voucherId <= 0) {
            throw new InvalidArgumentException(sprintf(
                '%s is not a valid VoucherId.', $voucherId
            ));
        }
        if (!is_numeric($voucher_value)) {
            throw new InvalidArgumentException(sprintf(
                '%s is not a valid Voucher Value.', $voucher_value
            ));
        }

        $document = $this->getDocumentById($doctypeId, $doctype);

        if (empty($document)) {
            throw new InvalidArgumentException(sprintf(
                '%s is not a valid %s.', $doctypeId, ($doctype === 'auftrag' ? 'Order' : 'Invoice')
            ));
        }
        if (empty($voucher)) {
            $voucher = $this->getById($voucherId);
        }
        if ($voucher['voucher_residual_value'] < $voucher_value) {
            throw new RuntimeException('Voucher could not be added to order.');
        }

        if ($voucher['only_for_customer'] && (int)$voucher['address_id'] !== (int)$document['adresse']) {
            throw new RuntimeException('Voucher-Address does not match to Order.');
        }

        if ($voucher['tax_name'] !== 'normal' && $voucher['tax_name'] !== 'ermaessigt') {
            $voucher['tax_name'] = 'befreit';
        }

        /** @deprecated-block-start */
        $voucher_value_net = -abs($voucher_value);
        if ($voucher['tax_name'] === 'ermaessigt') {
            $voucher_value_net /= $this->app->erp->GetSteuersatzErmaessigt(true, $doctypeId, $doctype);
        } elseif ($voucher['tax_name'] !== 'befreit') {
            $voucher_value_net /= $this->app->erp->GetSteuersatzNormal(true, $doctypeId, $doctype);
        }
        $article_name = $this->db->fetchValue(
            'SELECT art.name_de FROM artikel AS art WHERE id = :article_id',
            ['article_id' => $voucher['article_id']]
        );
        $isPosEmpty = empty($positionId);
        if ($doctype === 'rechnung') {
            if ($isPosEmpty) {
                $positionId = $this->app->erp->AddRechnungPositionManuell($doctypeId, $voucher['article_id'],
                    $voucher_value_net, 1,
                    $article_name, $voucher['voucher_code'], $voucher['currency']);
            } else {
                $this->db->perform('UPDATE `rechnung_position` SET preis = :price WHERE id = :position_id',
                    ['price' => $voucher_value_net, 'position_id' => $positionId]
                );
            }
        } elseif ($isPosEmpty) {
            $positionId = (int)$this->app->erp->AddAuftragPositionManuell($doctypeId, $voucher['article_id'],
                $voucher_value_net, 1,
                $article_name, $voucher['voucher_code'], $voucher['currency']);
        } else {
            $this->db->perform('UPDATE `auftrag_position` SET preis = :price WHERE id = :position_id',
                ['price' => $voucher_value_net, 'position_id' => $positionId]
            );
        }
        if (in_array($voucher['tax_name'], ['befreit', 'ermaessigt', 'normal'])) {
            if ($doctype === 'auftrag') {
                $this->db->perform('UPDATE `auftrag_position` SET umsatzsteuer = :tax_name WHERE id = :id',
                    ['id' => (int)$positionId, 'tax_name' => $voucher['tax_name']]
                );
            } else {
                $this->db->perform('UPDATE `rechnung_position` SET umsatzsteuer = :tax_name WHERE id = :id',
                    ['id' => (int)$positionId, 'tax_name' => $voucher['tax_name']]
                );
            }
        }
        /** @deprecated-block-end */
        if (empty($positionId)) {
            throw new RuntimeException('Voucher could not be added to order.');
        }

        $this->db->perform(
            'UPDATE voucher SET voucher_residual_value = voucher_residual_value - :voucher_residual_value WHERE id = :id',
            ['id' => (int)$voucherId, 'voucher_residual_value' => $voucher_value]
        );

        $this->db->perform(
            'INSERT INTO voucher_order (voucher_id, order_id, voucher_value, doctype, order_position_id) 
                VALUES (:voucher_id, :order_id, :order_value, :doctype, :order_position_id)',
            [
                'voucher_id'  => (int)$voucherId,
                'order_id'    => (int)$doctypeId,
                'order_position_id' => (int)$positionId,
                'order_value' => abs((float)$voucher_value),
                'doctype'     => $doctype
            ]
        );
        $insertId = (int)$this->db->lastInsertId();
        if ($insertId === 0) {
            if ($doctype === 'rechnung') {
                $this->db->perform('DELETE FROM rechnung_position WHERE id = :id', ['id' => $positionId]);
                throw new RuntimeException('Voucher could not be added to invoice.');
            }
            $this->db->perform('DELETE FROM auftrag_position WHERE id = :id', ['id' => $positionId]);
            throw new RuntimeException('Voucher could not be added to order.');
        }
        $this->recalculateResidualValue($voucherId);
        $this->changeAddress($voucherId, $document['adresse']);

        return $insertId;
    }

    /**
     * @param int $voucherId
     * @param int $adressId
     */
    private function changeAddress($voucherId, $adressId)
    {
        $voucher = $this->getById($voucherId);
        if (empty($voucher['address_id']) || $voucher['address_id'] !== $adressId) {
            $this->db->perform(
                'UPDATE voucher SET address_id = :address_id WHERE id = :id ',
                ['address_id' => $adressId, 'id' => $voucherId]
            );
        }
    }

    /**
     * @param int    $voucherId
     * @param string $doctype
     * @param int    $doctypeId
     * @param int    $positionId
     * @param int    $reedemed
     * @param float  $redeemedValue
     * @param int    $projectId
     * @param string $username
     */
    public function addVoucherPosLog(
        $voucherId,
        $doctype,
        $doctypeId,
        $positionId,
        $reedemed,
        $redeemedValue,
        $projectId = 0,
        $username = ''
    ) {
        if ((float)$redeemedValue === 0.0) {
            throw new InvalidArgumentException('Redeemed Value must not be 0.');
        }

        if ($doctypeId <= 0) {
            throw new InvalidArgumentException(sprintf(
                '%s is not a valid %s.', $doctypeId, ($doctype === 'auftrag' ? 'OrderId' : 'InvoiceId')
            ));
        }
        if ($voucherId <= 0) {
            throw new InvalidArgumentException(sprintf(
                '%s is not a valid VoucherId.', $voucherId
            ));
        }
        if (!is_numeric($redeemedValue)) {
            throw new InvalidArgumentException(sprintf(
                '%s is not a valid Voucher Value.', $redeemedValue
            ));
        }

        $this->getById($voucherId);

        $this->db->perform('INSERT INTO voucher_pos 
        (voucher_id, project_id, redeemed, redeemed_value, doctype, doctype_id, position_id, created_by) VALUES 
        (:voucher_id, :project_id, :redeemed,:reedemed_value, :doctype,:doctype_id, :position_id, :created_by)',
            [
                'voucher_id'     => (int)$voucherId,
                'project_id'     => (int)$projectId,
                'redeemed'       => (int)$reedemed,
                'reedemed_value' => abs((float)$redeemedValue),
                'doctype'        => (String)$doctype,
                'doctype_id'     => (int)$doctypeId,
                'position_id'    => (int)$positionId,
                'created_by'     => (String)$username,
            ]
        );
    }

    /**
     * @param      $voucherOrderId
     * @param bool $with_position
     *
     * @return bool true if successful
     */
    public function deleteVoucherFromOrder($voucherOrderId, $with_position = true)
    {
        try {
            $voucherOrder = $this->getVoucherOrderById($voucherOrderId);
            $orderPositionId = $this->getVoucherPositionIdFromOrder($voucherOrder['order_id'],
                $voucherOrder['voucher_id']);
            if (!$orderPositionId) {
                return false;
            }

            if ($with_position) {
                /** @deprecated-block-start */
                $doctype = $voucherOrder['doctype'] === 'rechnung' ? 'rechnung' : 'auftrag';
                $doctypePosition = $doctype . '_position';
                $this->app->YUI->SortListEvent('del', $doctypePosition, $doctype, $voucherOrder['order_id'],
                    $orderPositionId);
                /** @deprecated-block-end */
            }

            $affectedRows = $this->db->fetchAffected(
                'UPDATE voucher SET voucher_residual_value = voucher_residual_value - :voucher_residual_value WHERE id = :id',
                ['id' => (int)$voucherOrder['voucher_id'], 'voucher_residual_value' => $voucherOrder['voucher_value']]
            );
            if ($affectedRows === 1) {
                $this->db->perform('DELETE FROM voucher_order WHERE id = :id', ['id' => $voucherOrder['id']]);
                $this->recalculateResidualValue($voucherOrder['voucher_id']);

                return true;
            }

        } catch (Exception $e) {
            return false;
        }

        return false;
    }

    /**
     * @param string      $code
     * @param int         $articleId
     * @param float       $voucherValue
     * @param int         $invoiceId
     * @param int         $invoicePositionId
     * @param string      $taxName
     * @param string      $currency
     * @param string|null $validFrom
     * @param string|null $validTo
     * @param bool        $showCodeOnDocument
     * @param bool        $onlyForCustomer
     * @param int         $addressId
     *
     * @return int voucher_id
     */
    public function create(
        $code,
        $articleId,
        $voucherValue,
        $invoiceId,
        $invoicePositionId,
        $taxName,
        $currency = 'EUR',
        $validFrom = null,
        $validTo = null,
        $showCodeOnDocument = true,
        $onlyForCustomer = false,
        $addressId = 0
    ) {
        try {
            $voucher = $this->getVoucherByCode($code);
            if (!empty($voucher)) {
                throw new RuntimeException('Voucher with code allready exists');
            }

            if ($taxName !== 'normal' && $taxName !== 'ermaessigt') {
                $taxName = 'befreit';
            }

            $this->db->perform('INSERT INTO voucher 
            (voucher_date, article_id, voucher_code, voucher_original_value,voucher_residual_value, 
            valid_from,valid_to, invoice_id, invoice_position_id, tax_name,currency,show_code_on_document, only_for_customer,address_id) 
            VALUES (now(), :article_id ,:voucher_code, :voucher_value, :voucher_value, 
            :valid_from, :valid_to, :invoice_id, :invoice_position_id , :tax_name, :currency, :show_code_on_document,:only_for_customer,:address_id)',
                [
                    'article_id'            => $articleId,
                    'voucher_code'          => $code,
                    'voucher_value'         => (float)$voucherValue,
                    'valid_from'            => $validFrom,
                    'valid_to'              => $validTo,
                    'invoice_id'            => $invoiceId,
                    'invoice_position_id'   => $invoicePositionId,
                    'tax_name'              => (String)$taxName,
                    'currency'              => !empty($currency) ? $currency : 'EUR',
                    'show_code_on_document' => $showCodeOnDocument ? 1 : 0,
                    'only_for_customer'     => $onlyForCustomer ? 1 : 0,
                    'address_id'            => (int)$addressId,
                ]
            );

            return (int)$this->db->lastInsertId();
        } catch (Exception $e) {
            throw new RuntimeException('Could not create Voucher' . $e->getMessage());
        }
    }

    /**
     * @param int $voucherId
     *
     * @return bool
     */
    public function delete($voucherId)
    {
        $voucher_orders = $this->getVoucherOrdersFromVoucher($voucherId, 1);
        if (empty($voucher_orders)) {
            $affectedRows = $this->db->fetchAffected('DELETE FROM voucher WHERE id = :id', ['id' => $voucherId]);

            return $affectedRows === 1;
        }
        throw new RuntimeException('Voucher is allready associated with order(s)');
    }

    /**
     * @param int $voucherId
     */
    public function recalculateResidualValue($voucherId)
    {
        $residualValue = $this->getResidualValueFromVoucherId($voucherId);
        $this->db->perform('UPDATE voucher SET voucher_residual_value = :voucher_residual_value WHERE id = :id'
            , ['voucher_residual_value' => $residualValue, 'id' => $voucherId]);
    }

    /**
     * @param array $voucher
     * @param int   $projectId
     * @param float $value
     *
     * @return array
     */
    public function getArticleForPos($voucher, $projectId, $value)
    {
        $sql = '
      SELECT 
        art.id, 
        art.nummer, 
        art.projekt, 
        art.name_de AS artikel, 
        art.kurztext_de, 
        art.umsatzsteuer,
        art.rabatt AS rabattartikel,
        art.rabatt_prozent,
        art.anabregs_text,
        IF(art.seriennummern=\'keine\',\'\',IFNULL(art.seriennummern,\'\')) AS seriennummern,
        art.porto,
        ifnull(art.keinrabatterlaubt,0) AS keinrabatterlaubt
      FROM 
        artikel AS art
      WHERE 
        art.id = :article_id
      ';

        $row = $this->db->fetchRow($sql, [
            'article_id' => $voucher['article_id'],
        ]);

        $row['anabregs_text'] = $voucher['voucher_code'];
        $row['keinrabatterlaubt'] = 1;
        $row['rabatt_prozent'] = 0;
        $row['rabatt'] = 0;

        $erloes = '';
        $steuersatz = null;
        $steuertext = null;
        $umsatzsteuerpos = null;
        $this->app->erp->GetArtikelSteuer(
            $voucher['article_id'],
            0,
            false,
            $steuersatz,
            $steuertext,
            $erloes,
            $umsatzsteuerpos,
            null,
            $projectId
        );
        $row['erloes'] = $erloes;

        $row['umsatzsteuer'] = !empty($voucher['tax_name']) ? $voucher['tax_name'] : 'normal';
        if ($row['umsatzsteuer'] === 'befreit') {
            $row['tax'] = '0%';
        } elseif ($row['umsatzsteuer'] === 'ermaessigt') {
            $row['tax'] = round($this->app->erp->GetStandardSteuersatzErmaessigt($projectId), 2) . '%';
        } else {
            $row['tax'] = round($this->app->erp->GetStandardSteuersatzNormal($projectId), 2) . '%';
        }
        $row['preis'] = -$value;

        return $row;
    }

    /**
     * @return array
     */
    public function getValidTypes()
    {
        return $this->validtypes;
    }

    /**
     * @param string $validType
     *
     * @return string|null
     */
    public function getValidToFromValidType($validType)
    {
        try {
            $date = new DateTime(date('Y-m-d'));
            if (strpos($validType, 'ThisYear') === 0) {
                $date = new DateTime(date('Y-12-31'));
                $plusYears = substr($validType, 8);
                if (empty($plusYears)) {
                    return $date->format('Y-m-d');
                }
                if (strlen($plusYears) >= 5 && strpos($plusYears, 'Plus') === 0) {
                    $date->add(new DateInterval('P' . substr($plusYears, 4) . 'Y'));

                    return $date->format('Y-m-d');
                }
            } elseif (strlen($validType) > 4 && strpos($validType, 'Year') === 0) {
                $plusYears = (int)substr($validType, 4);
                if ($plusYears <= 0) {
                    return null;
                }
                $date->add(new DateInterval('P' . $plusYears . 'Y'));

                return $date->format('Y-m-d');
            } elseif (strlen($validType) > 5 && strpos($validType, 'Month') === 0) {
                $plusMonth = (int)substr($validType, 5);
                if ($plusMonth <= 0) {
                    return null;
                }
                $date->add(new DateInterval('P' . $plusMonth . 'M'));

                return $date->format('Y-m-d');
            }
        } catch (Exception $e) {

        }

        return null;
    }

    /**
     * @param string $columnname
     *
     * @return string
     */
    public function getValidTypeTranslationSql($columnname = 'vt.valid')
    {
        $sql = '';
        foreach ($this->validtypes as $validkey => $validDescriotion) {
            $sql .= sprintf('if(%s = \'%s\',\'%s\',', $columnname, $validkey, $validDescriotion);
        }
        $sql .= '\'\'' . str_repeat(')', count($this->validtypes));

        return $sql;
    }

    /**
     * @param int $voucherId
     *
     * @return string
     */
    public function getCodeForDocumentByVoucherId($voucherId)
    {
        $voucher = $this->getById($voucherId);

        return (int)$voucher['show_code_on_document'] === 1 ? $voucher['voucher_code'] : '';
    }

    /**
     * @param int $invoiceId
     * @param int $positionId
     *
     * @return array
     */
    public function findCodesForDocumentByInvoiceIdAndPositionId(int $invoiceId, int $positionId): array
    {
        $sql =
            'SELECT v.voucher_code
            FROM `voucher` AS `v`
            WHERE v.invoice_position_id = :invoice_position_id
            AND v.invoice_id = :invoice_id
            AND v.show_code_on_document = 1';

        $results = $this->db->fetchAll(
            $sql,
            [
                'invoice_position_id' => $positionId,
                'invoice_id'          => $invoiceId,
            ]
        );
        $codes = [];
        if (!empty($results)) {
            foreach ($results as $result) {
                $codes[] = $result['voucher_code'];
            }
        }

        return $codes;
    }

    /**
     * @param int $invoiceId
     *
     * @return array
     */
    public function getVouchersByInvoiceId($invoiceId)
    {
        return $this->db->fetchAll(
            'SELECT v.* FROM voucher AS v WHERE v.invoice_id = :invoice_id',
            ['invoice_id' => $invoiceId]
        );
    }

    /**
     * @param string $date
     * @param int    $projectId
     *
     * @return array
     */
    public function getPosVouchersByDate($date, $projectId = 0)
    {
        return $this->db->fetchAll(
            'SELECT v.* 
            FROM voucher AS v 
            INNER JOIN rechnung AS i ON v.invoice_id = i.id
            INNER JOIN pos_order AS po ON i.id = po.rechnung
            WHERE DATE(po.zeitstempel) = :date AND (po.projekt = :project_id OR 0 = :project_id)',
            ['date' => $date, 'project_id' => $projectId]
        );
    }

    /**
     * @param string $date
     * @param int    $projectId
     * @param string $status
     * @param string $paymentType
     *
     * @return array
     */
    public function getPosVoucherSumByDateTax($date, $projectId = 0, $status = '', $paymentType = '')
    {
        return $this->db->fetchAll(
            'SELECT SUM(
                IF(
                    ip.umsatz_brutto_gesamt <> 0,
                    ip.umsatz_brutto_gesamt,
                    ip.menge*ip.preis*(1-ip.rabatt / 100) 
                        * IF(
                            i.ust_befreit > 1 OR (i.ust_befreit = 1 AND i.ustid != \'\') 
                            OR ip.umsatzsteuer = \'befreit\' OR ip.steuersatz = 0,
                            0, IF(ip.steuersatz > 0,ip.steuersatz,IF(ip.umsatzsteuer = \'ermaessigt\', 
                                i.steuersatz_ermaessigt, i.steuersatz_normal))
                    )/ 100
                )
            ) AS amount, 
            IF(
                i.ust_befreit > 1 OR (i.ust_befreit = 1 AND i.ustid != \'\') 
                    OR ip.umsatzsteuer = \'befreit\' OR ip.steuersatz = 0,
                0, IF(ip.steuersatz > 0,ip.steuersatz,IF(ip.umsatzsteuer = \'ermaessigt\',
                    i.steuersatz_ermaessigt, i.steuersatz_normal))
            ) AS tax
            FROM voucher AS v 
            INNER JOIN rechnung AS i ON v.invoice_id = i.id AND (i.status = :status OR :status = \'\') 
                                            AND (i.zahlungsweise = :payment OR :payment = \'\')
            INNER JOIN rechnung_position AS ip ON i.id = ip.rechnung AND v.article_id = ip.artikel
            INNER JOIN pos_order AS po ON i.id = po.rechnung
            WHERE DATE(po.zeitstempel) = :date AND (po.projekt = :project_id OR 0 = :project_id)
            GROUP BY tax
            ORDER BY tax',
            ['date' => $date, 'project_id' => $projectId, 'status' => (String)$status, 'payment' => $paymentType]
        );
    }


    /**
     * @param string $date
     * @param int    $projectId
     * @param string $status
     *
     * @return float
     */
    public function getPosVoucherSumByDate($date, $projectId = 0, $status = '')
    {
        return (float)$this->db->fetchValue(
            "SELECT SUM(v.voucher_original_value) 
            FROM voucher AS v 
            INNER JOIN rechnung AS i ON v.invoice_id = i.id AND (i.status = :status OR :status = '')
            INNER JOIN pos_order AS po ON i.id = po.rechnung
            WHERE DATE(po.zeitstempel) = :date AND (po.projekt = :project_id OR 0 = :project_id)",
            ['date' => $date, 'project_id' => $projectId, 'status' => (String)$status]
        );
    }

    /**
     * @param string $date
     * @param int    $projectId
     * @param string $status
     *
     * @return float
     */
    public function getReedemedPosSumByDate($date, $projectId = 0, $status = '')
    {
        return (float)$this->db->fetchValue(
            "SELECT SUM(vo.voucher_value) 
            FROM pos_order AS po
            INNER JOIN auftrag AS o ON po.auftrag = o.id
            INNER JOIN rechnung AS i ON o.id = i.auftragid AND po.rechnung = i.id AND (i.status = :status OR :status = '')
            INNER JOIN voucher_order AS vo 
                ON (o.id = vo.order_id AND vo.doctype <> 'rechnung' OR i.id = vo.voucher_id AND vo.doctype = 'rechnung')
            WHERE DATE(po.zeitstempel) = :date AND (po.projekt = :project_id OR 0 = :project_id)",
            ['date' => $date, 'project_id' => $projectId, 'status' => (String)$status]
        );
    }

    /**
     * @param int   $invoiceId
     * @param array $positionArr
     *
     * @return array
     */
    public function createVoucherFromInvoicePositions($invoiceId, $positionArr)
    {
        if (empty($invoiceId) || empty($positionArr)) {
            return $positionArr;
        }

        $positionIdKeyArr = [];
        foreach ($positionArr as $row) {
            $positionIdKeyArr[$row['position_id']] = $row;
        }

        $neededVouchers = $this->getNeededVouchersFromInvoice($invoiceId, $positionIdKeyArr);

        if (empty($neededVouchers)) {
            return $positionArr;
        }

        foreach ($neededVouchers as $neededVoucherKey => $neededVoucher) {
            $template = $this->getTemplateFromId($neededVoucher['template_id']);
            for ($i = 1; $i <= $neededVoucher['amount']; $i++) {
                $neededVouchers[$neededVoucherKey]['voucherId'] = $this->createVoucherFromTemplate($invoiceId,
                    $neededVoucher['position_id'], $neededVoucher['price_gross'], $neededVoucher['currency'],
                    $template);
            }
        }

        return $neededVouchers;
    }

    /**
     * @param int $templateId
     *
     * @return array
     */
    public function getTemplateFromId($templateId)
    {
        $sql = 'SELECT * FROM voucher_template WHERE id = :id';

        $ret = $this->db->fetchRow($sql, ['id' => $templateId]);
        if ($ret['tax_name'] !== 'ermaessigt' && $ret['tax_name'] !== 'normal') {
            $ret['tax_name'] = 'befreit';
        }

        return $ret;
    }

    /**
     * @param int    $invoiceId
     * @param int    $invoicePositionId
     * @param float  $price
     * @param string $currency
     * @param array  $template
     *
     * @return int
     */
    public function createVoucherFromTemplate($invoiceId, $invoicePositionId, $price, $currency, $template)
    {
        if (empty($invoiceId) || empty($price) || empty($template)) {
            return 0;
        }
        $code = $this->getNewVoucherCodeFromPattern($template['code_pattern']);
        if (empty($code)) {
            return 0;
        }
        $validFrom = null;
        $validTo = $this->getValidToFromValidType($template['valid']);
        if ($validTo !== null) {
            $validFrom = date('Y-m-d');
        }
        $addressId = $this->db->fetchValue(
            'SELECT adresse FROM rechnung WHERE id = :invoice_id LIMIT 1',
            ['invoice_id' => (int)$invoiceId]
        );

        return $this->create($code, $template['article_id'], $price, $invoiceId, $invoicePositionId,
            $template['tax_name'], $currency, $validFrom, $validTo, $template['show_code_on_document'] ? true : false,
            $template['only_for_customer'] ? true : false, $addressId);
    }

    /**
     * @param int   $invoiceId
     * @param array $articleAmounts
     *
     * @return array
     */
    public function getNeededVouchersFromInvoice($invoiceId, $invoicePosData)
    {
        $neededVouchers = [];
        if (empty($invoiceId) || empty($invoicePosData)) {
            return [];
        }

        //amount_diff = invoice amount - amount of voucher codes for invoice position
        $sql =
            'SELECT 
                rp.id AS `position_id`,
                rp.artikel AS `article_id`, 
                rp.menge - IFNULL(COUNT(v.id),0) AS `amount_diff`,
                vt.id AS `template_id`
            FROM `rechnung_position` AS `rp` 
            LEFT JOIN `voucher` AS `v` ON v.invoice_position_id = rp.id
            INNER JOIN `voucher_template` AS `vt` ON vt.article_id = rp.artikel
            WHERE rp.rechnung = :invoice_id
            GROUP BY rp.id';

        $voucherData = $this->db->fetchAll($sql, ['invoice_id' => $invoiceId]);

        if (empty($voucherData)) {
            return [];
        }
        foreach ($voucherData as $data) {
            $positionId = $data['position_id'];
            $priceGross = $invoicePosData[$positionId]['price_gross'];
            $taxName = $invoicePosData[$positionId]['tax_name'];
            if ($data['amount_diff'] > 0) {
                $neededVouchers[] = [
                    'template_id' => $data['template_id'],
                    'price_gross' => $priceGross,
                    'amount'      => $data['amount_diff'],
                    'tax_name'    => $taxName,
                    'position_id' => $positionId,
                ];
            }
        }

        return $neededVouchers;
    }

    /**
     * @param int    $count
     * @param string $type
     *
     * @return null|string
     */
    private function genCode($count, $type)
    {
        if ($count <= 0) {
            return null;
        }
        $code = '';
        if ($type === 'num') {
            $chararr = $this->digits;
        } elseif ($type === 'anum') {
            $chararr = array_merge($this->digits, $this->alphas);
        } elseif ($type === 'alpha') {
            $chararr = $this->alphas;
        } else {
            return null;
        }
        $upper = count($chararr) - 1;
        for ($i = 0; $i < $count; $i++) {
            $chr = $chararr[mt_rand(0, $upper)];
            while ($i === 0 && $chr === '0') {
                $chr = $chararr[mt_rand(0, $upper)];
            }
            $code .= $chr;
        }

        return $code;
    }

    /**
     * @param string $pattern
     *
     * @return string|null
     */
    public function getNewVoucherCodeFromPattern($pattern)
    {
        $numcount = 0;
        $subpattern = '';
        $patterns = ['num', 'anum', 'alpha'];
        foreach ($patterns as $patterntype) {
            $plen = strlen($patterntype);
            if (strpos($pattern, $patterntype) === 0) {
                $numcount = (int)substr($pattern, $plen);
                $subpattern = $patterntype;
                break;
            }
        }

        if ($numcount <= 0) {
            return null;
        }

        $code = $this->genCode($numcount, $subpattern);
        if ($code === null) {
            return null;
        }

        while (
        $this->db->fetchValue('
                SELECT id 
                FROM voucher 
                WHERE voucher_code = :code',
            ['code' => $code]
        )
        ) {
            $code = $this->genCode($numcount, $subpattern);
        }

        return $code;
    }
}