<?php

namespace Velis\Db;

use ArrayObject;
use PDOException;
use Velis\App;
use Velis\Lang;

/**
 * Extension to PDO Exception class
 * @author Olek Procki <olo@velis.pl>
 */
class Exception extends PDOException
{
    /**
     * Db error codes
     */
    const CODE_CONSTRAINT_VIOLATION = 23514;
    const CODE_DUPLICATED = 23505;
    const CODE_FOREIGN = 23503;
    const CODE_NOT_NULL = 23502;
    const CODE_VALUE_TOO_LONG = 22001;
    const CODE_NUMERIC_OUT_OF_RANGE = 22003;


    /**
     * Database (schema)
     * @var string
     */
    protected $_sqlName;

    /**
     * SQL query code
     * @var string
     */
    protected $_sql;

    /**
     * Query bind params
     * @var array|ArrayObject
     */
    protected $_params;

    /**
     * Time of exception creation
     * @var string
     */
    protected $_createdTime;

    /**
     * Database original error info
     * @var string
     */
    protected $_error;

    /**
     * Constructor
     *
     * @param string $message
     * @param int $code
     * @param string $sqlName
     * @param string $sql
     * @param array|ArrayObject $params
     */
    public function __construct($message = '', $code = 0, $sqlName = 'unknown', $sql = '', $params = null)
    {
        $this->_error          = $message;
        $this->_sqlName        = $sqlName;
        $this->_sql            = $sql;
        $this->_params         = $params;
        $this->_createdTime    = date('Y-M-d H:i:s');

        if (mb_strpos($message, 'Raise exception:')) {
            if (mb_strpos($message, 'ERROR:') !== false) {
                $message = mb_substr($message, mb_strpos($message, 'ERROR:') + 6);
            } elseif (str_contains($message, 'BŁĄD:')) {
                $message = mb_substr($message, mb_strpos($message, 'BŁĄD:') + 5);
            }

            if (mb_strpos($message, 'CONTEXT:')) {
                $message = mb_substr($message, 0, mb_strpos($message, 'CONTEXT:'));
            } elseif (strpos($message, 'KONTEKST:')) {
                $message = mb_substr($message, 0, mb_strpos($message, 'KONTEKST:'));
            }
            $message = Lang::getHistoryTranslation($message);
        }

        parent::__construct($message, (int)$code);
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return [
            'error' => $this->_error,
            'sql' => $this->_sql,
            'params' => $this->_params,
            'created_time' => $this->_createdTime,
            'sql_name' => $this->_sqlName,
        ];
    }

    /**
     * Returns SQL query
     * @return string
     */
    public function getSQL()
    {
        return $this->_sql;
    }


    /**
     * Returns SQL query params
     * @return array|ArrayObject
     */
    public function getParams()
    {
        return $this->_params;
    }


    /**
     * Returns original database error info
     * @return string
     */
    public function getError()
    {
        return $this->_error;
    }

    /**
     * Returns message truncated to the first line, dropping any CONTEXT information from PL/SQL functions
     * @return string
     */
    public function getNoStackMessage()
    {
        $message = $this->getMessage();
        return current(explode("\n", $message));
    }


    /**
     * Outputs exception info
     *
     * @param bool $showSqlError
     * @return string
     */
    public function output(bool $showSqlError = false): string
    {
        $counter = 0;

        $eDetails = __CLASS__ . "\n";
        $eDetails .= "Database: " . $this->_sqlName . "\n";

        if (App::devMode() || App::isSuper() || App::isTesting()) {
            $eDetails .= "Message: " . $this->getMessage() . "\n";
        } elseif ($showSqlError) {
            $eDetails .= "Message: " . $this->_error . "\n";
        }

        $eDetails .= "Code: " . $this->getCode() . "\n\n";
        $eDetails .= "SQL:\n" . $this->_sql . "\n\n";

        if ($this->_params != null) {
            $eDetails .= "Bind params:\n" . print_r($this->_params, true) . "\n\n";
        }

        $eDetails .= "Stack trace:\n";

        foreach ($this->getTrace() as $trace) {
            $eDetails .= '#' . $counter++;

            if (isset($trace['file'])) {
                $eDetails .= ' ' . $trace['file'];
            }

            if (isset($trace['line'])) {
                $eDetails .= '(' . $trace['line'] . ') ';
            }

            $eDetails .= ' ';

            if (isset($trace['class'])) {
                $eDetails .= $trace['class'] . $trace['type'];
            }

            $eDetails .= $trace['function'] . '(';
            $eDetails .= ");\n";
        }

        return $eDetails;
    }


    /**
     * Returns string exception representation
     * @return string
     */
    public function __toString()
    {
        return $this->output(!ini_get('display_errors'));
    }
}
