<?php
/**
 * Item line in the Invoice table, relates to invoice_item table
 *
 * {autogenerated}
 * @property int $invoice_item_id PK
 * @property int $invoice_id FK invoice
 * @property string|null $invoice_public_id
 * @property int|null $item_id
 * @property string $item_type
 * @property string $item_title
 * @property string $item_description
 * @property int $qty
 * @property float $first_price
 * @property float $first_discount
 * @property float $first_tax
 * @property float $first_total
 * @property float $first_shipping
 * @property string $first_period
 * @property int|null $rebill_times
 * @property float|null $second_price
 * @property float|null $second_discount
 * @property float|null $second_tax
 * @property float|null $second_total
 * @property float|null $second_shipping
 * @property string|null $second_period
 * @property string|null $currency
 * @property string|null $tax_group
 * @property string|null $tax_rate
 * @property int $is_countable
 * @property int $variable_qty
 * @property int|null $is_tangible
 * @property int|null $billing_plan_id
 * @property string|null $billing_plan_data
 * @property string|null $options
 * @property string|null $option1
 * @property string|null $option2
 * @property string|null $option3
 * {/autogenerated}
 * @package Am_Invoice
 */
class InvoiceItem extends Am_Record_WithData
{
    /** @var IProduct */
    protected $_product = null;

    /** @var array decoded options from "options" field */
    protected $_options = [];
    protected $_optionPrices = [];
    /**
     * Constant array of fields to copy from product to invoice on add
     * @var array
     */
    protected static $_productInvoiceFields = [
        'productId'     => 'item_id',
        //'title'         => 'item_title',
        'type'          => 'item_type',
        //'description'   => 'item_description',

        'firstPrice'    => 'first_price',
        'firstPeriod'   => 'first_period',

        'rebillTimes'   => 'rebill_times',

        'secondPrice'   => 'second_price',
        'secondPeriod'  => 'second_period',

        'currencyCode'  => 'currency',

        'taxGroup'      => 'tax_group',
        'isTangible'    => 'is_tangible',
        'isCountable'   => 'is_countable',
        'isVariableQty' => 'variable_qty',
        'billingPlanId' => 'billing_plan_id',
    ];

    /**
     * Set fields from a product record
     * @return InvoiceItem provides fluent interface
     */
    public function copyProductSettings(IProduct $p, array $options = [])
    {
        $this->setOptionsFromArray($options);

        $this->item_title = $p->getTitle(false);
        $this->item_description = $p->getDescription(false);

        foreach (self::$_productInvoiceFields as $kp => $ki) {
            $this->$ki = call_user_func([$p, 'get'.ucfirst($kp)]);
            if (strpos($kp, 'is')===0)
                $this->$ki = (bool)$this->$ki;
        }
        if (! $p->getRebillTimes()) {
            $this->second_price  = null;
            $this->second_period = null;
        }

        $this->setBillingPlanData($p->getBillingPlanData());

        if (method_exists($p, 'applyOptionsToPrices'))
        {
            $p->applyOptionsToPrices($this->_options);
            foreach ($this->_options as $k => $option)
            {
                $this->first_price += $option['first_price'];
                $this->second_price += $option['second_price'];
            }
            if ($this->first_price<0) $this->first_price = 0;
            if ($this->second_price<0) $this->second_price = 0;
            $this->first_price = moneyRound($this->first_price);
            $this->second_price = moneyRound($this->second_price);
        }

        $this->_product = $p;

        return $this;
    }

    /**
     * Add given number to qty
     * if !is_countable, always set to 1
     */
    function add($qty)
    {
        $qty = intval($qty);
        if ($qty < 0)
            throw new Am_Exception_InvalidRequest("_qty_is_negative_in_InvoiceItem_add #". (int)$qty);
        $item = $this->tryLoadProduct();
        if (empty($this->qty))
            $this->qty = 0;
        $this->qty = $item->addQty($qty, $this->qty);
    }

    /**
     * Changing options in runtime does not affect prices
     * @param array $options
     * @return \InvoiceItem
     */
    protected function setOptionsFromArray(array $options)
    {
        $this->_options = [];
        foreach ($options as $k => $v)
        {
            $this->_options[$k] = [
                'value' => $v['value'],
                'optionLabel' => $v['optionLabel'],
                'valueLabel' => $v['valueLabel'],
                'first_price' => null,
                'second_price' => null,
            ];
        }
        $this->options = json_encode($this->_options, true);
        return $this;
    }

    public function tryLoadProduct(): ?IProduct
    {
        if (empty($this->_product) && ($this->item_id>0)) {
            $this->_product = $this->getTable()->loadItem($this->item_type, $this->item_id, false,
                    empty($this->billing_plan_id) ? null : $this->billing_plan_id);
        }
        return $this->_product;
    }

    /**
     * Do arithmetic operations to calculate subtotal and total
     */
    public function _calculateTotal()
    {
        $this->first_total = moneyRound($this->first_price * $this->qty)
                         - $this->first_discount
                         + $this->first_shipping
                         + $this->first_tax;
        $this->second_total = moneyRound($this->second_price * $this->qty)
                         - $this->second_discount
                         + $this->second_shipping
                         + $this->second_tax;
    }

    /**
     * Add access period for current product based on information from incoming paysystem transaction
     * @throws Am_Exception_Db_NotUnique
     */
    public function addAccessPeriod($isFirstPayment, Invoice $invoice, Am_Paysystem_Transaction_Interface $transaction, $beginDate, $invoicePaymentId)
    {
        if ($this->item_type != 'product' || $this->data()->get('no-access'))
            return; // if that is not a product then no access

        $a = $this->getDi()->accessRecord;
        $a->setDisableHooks(true);
        $a->begin_date = $beginDate;
        $p = new Am_Period($isFirstPayment ? $this->first_period : $this->second_period);
        $a->invoice_id = $this->invoice_id;
        $a->invoice_public_id = $this->invoice_public_id;
        $recurringType = $transaction->getRecurringType();
        if (in_array($recurringType, [Am_Paysystem_Abstract::REPORTS_EOT, Am_Paysystem_Abstract::REPORTS_NOTHING]) && $invoice->rebill_times > 0) {
            $a->expire_date = Am_Period::RECURRING_SQL_DATE;
        } else {
            $a->expire_date = $p->addTo($a->begin_date);
        }
        if ($this->data()->get('begin_date')) {
            $a->begin_date = $this->data()->get('begin_date');
            $a->expire_date = $this->data()->get('expire_date');
        }
        $a->product_id = $this->item_id;
        $a->user_id = $invoice->user_id;
        $a->transaction_id = $transaction->getUniqId();
        $a->invoice_payment_id = $invoicePaymentId;
        $a->invoice_item_id = $this->pk();
        $a->qty = $this->qty;
        $a->insert();
    }

    function getFirstSubtotal()
    {
        return moneyRound($this->first_price * $this->qty);
    }

    function getFirstTotal()
    {
        return $this->getFirstSubtotal() - $this->first_discount;
    }

    function getSecondSubtotal()
    {
        return moneyRound($this->second_price * $this->qty);
    }
    function getSecondTotal()
    {
        return $this->getSecondSubtotal() - $this->second_discount;
    }

    function setBillingPlanData(array $data)
    {
        $this->billing_plan_data = serialize((array)$data);
    }

    /**
     * @return array|mixed entire array or requested $key value
     */
    function getBillingPlanData($key = null)
    {
        $arr = empty($this->billing_plan_data) ? [] :
            unserialize($this->billing_plan_data);
        if ($key === null) return $arr;
        return empty($arr[$key]) ? null : $arr[$key];
    }

    /**
     * Replace product in already existing invoice
     * throw exception in case of error
     */
    function replaceProduct($productId, $billingPlanId)
    {
        $old_product_id = $this->item_id;

        $p = $this->getDi()->productTable->load((int)$productId);
        $this->item_id = $p->pk();
        $this->item_description = $p->description;
        $this->item_title = $p->title;
        $this->billing_plan_id = (int)$billingPlanId;
        $this->update();
        // replace access records
        $accessRecords = $this->getDi()->accessTable->findBy([
            'invoice_id' => $this->invoice_id,
            'product_id' => $old_product_id,
        ]);
        foreach ($accessRecords as $access) {
            $access->product_id = $productId;
            $access->update();
        }
        $this->getDi()->invoiceTable->load($this->invoice_id)->getUser()->checkSubscriptions();
    }

    /**
     * Return options for $instance from qty of items. If options are
     * not set for given $instance, options for $instance=0 returned;
     * @param type $instance
     */
    public function getOptions()
    {
        if (empty($this->_options))
            if (empty($this->options))
                return [];
            else
                $this->_options = json_decode($this->options, true);
        if (!empty($this->_options))
            return $this->_options;
        else
            return [];
    }

    public function insert($reload = true)
    {
        if (!empty($this->_options))
            $this->options = json_encode($this->_options, true);
        $ret = parent::insert($reload);
        $ins = [];
        foreach ($this->getOptions() as $k => $v)
        {
            $ins[] = $this->getAdapter()->expandPlaceholders([
                    "(?a)",
                [
                    $this->pk(),
                    $this->invoice_id,
                    !empty($this->user_id) ? $this->user_id : null,
                    $this->item_id,
                    $k,
                    is_array($v['value']) ? implode(',', $v['value']) : $v['value'],
                    empty($v['first_price']) ? null : $v['first_price'],
                    empty($v['second_price']) ? null : $v['second_price'],
                ]
                ]
            );
        }
        if ($ins)
            $this->getAdapter()->query(
                "INSERT INTO ?_invoice_item_option "
                . "(invoice_item_id, invoice_id,user_id,item_id,name,value,first_price,second_price) "
                . "VALUES "
                . implode(',', $ins)
            );
        return $ret;
    }
}

/**
 * @extends Am_Table_WithData<InvoiceItem>
 * @method InvoiceItem[] findByInvoiceId($value, $start = null, $count = null, $orderBy = null)
 */
class InvoiceItemTable extends Am_Table_WithData
{
    protected $_key = 'invoice_item_id';
    protected $_table = '?_invoice_item';
    protected $_recordClass = 'InvoiceItem';

    protected $itemTypes = [
        'product' => "productTable",
    ];

    function getItemLoaders()
    {
        return $this->itemTypes;
    }

    function registerItemLoader($item_type, $class)
    {
        $this->itemTypes[$item_type] = $class;
    }

    /**
     * Load reference to class
     */
    function loadItem($item_type, $item_id, $throwExceptionIfNotFound = false, $billing_plan_id = null)
    {
        if (empty($this->itemTypes[$item_type]))
            return null;
        $ret = $this->getDi()->getService($this->itemTypes[$item_type])->load($item_id, $throwExceptionIfNotFound);
        if (($billing_plan_id > 0) && $ret)
        {
           if (!($bp = $this->getDi()->billingPlanTable->load($billing_plan_id, $throwExceptionIfNotFound)))
               return null;
           $ret->setBillingPlan($bp);
        }
        return $ret;
    }
}