<?php

namespace Leantime\Core\Db;

use Illuminate\Contracts\Container\BindingResolutionException;
use Leantime\Core\Database;
use Leantime\Core\Db\Db as DbCore;
use Leantime\Core\Events\DispatchesEvents;
use PDO;
use PDOStatement;
use ReflectionClass;
use ReflectionProperty;

/**
 * Repository
 */
abstract class Repository
{
    use DispatchesEvents;

    protected string $entity;

    protected string $model;

    public function __construct(
        private DbCore $db
    ) {}

    /**
     * dbcall - creates a new dbcall object
     *
     * @param  array  $args  - usually the value of func_get_args(), gives events/filters values to work with
     */
    protected function dbcall(array ...$args): object
    {
        return new class($args, $this)
        {
            private PDOStatement $stmn;

            private array $args;

            private Repository $caller_class;

            private Db $Db;

            /**
             * @var \Closure|mixed|object|null
             */
            private mixed $db;

            /**
             * constructor
             *
             * @param  array  $args  - usually the value of func_get_args(), gives events/filters values to work with
             * @param  Repository  $caller_class  - the class object that was called
             */
            public function __construct(array $args, Repository $caller_class)
            {
                $this->args = $args;
                $this->caller_class = $caller_class;
                // Use the singleton instance of Db to ensure connection pooling
                $this->db = app()->get(Db::class);
            }

            /**
             * prepares sql for entry; wrapper for PDO\prepare()
             *
             * @param  array  $args  - additional arguments to pass along to prepare function
             */
            public function prepare(string $sql, array $args = []): void
            {
                $sql = $this->caller_class::dispatch_filter(
                    'sql',
                    $sql,
                    $this->getArgs(['prepareArgs' => $args]),
                    4
                );

                $this->stmn = $this->db->database->prepare($sql, $args);
            }

            /**
             * binds values for search/replace of sql; wrapper for PDO\bindValue()
             *
             * @param  string  $needle  - placeholder to replace
             * @param  string  $replace  - value to replace with
             * @param  int  $type  - type of value being replaced
             */
            public function bindValue(string $needle, mixed $replace, int $type = PDO::PARAM_STR): void
            {
                $replace = $this->caller_class::dispatch_filter(
                    'binding.'.str_replace(':', '', $needle),
                    $replace,
                    $this->getArgs(),
                    4
                );

                $this->stmn->bindValue($needle, $replace, $type);
            }

            /**
             * executes the sql call - uses \PDO
             */
            public function lastInsertId(): mixed
            {
                return $this->db->database->lastInsertId();
            }

            /**
             * executes the sql call - uses \PDO
             */
            public function setFetchMode($mode, $class): bool
            {
                return $this->stmn->setFetchMode($mode, $class);
            }

            /**
             * Gets the arguments to pass along to events/filter
             *
             * @param  array  $additions  - any other additional parameters to include
             */
            private function getArgs(array $additions = []): array
            {
                $args = array_merge($this->args, ['self' => $this]);

                if (! empty($additions)) {
                    $args = array_merge($args, $additions);
                }

                $this->caller_class::dispatch_filter('args', $args, [], 5);

                return $args;
            }

            /**
             * executes the sql call - uses \PDO
             */
            public function __call(string $method, $arguments): mixed
            {
                if (! isset($this->stmn)) {
                    throw new \Error("You must run the 'prepare' method first!");
                }

                if (! in_array($method, ['execute', 'fetch', 'fetchAll'])) {
                    throw new \Error('Method does not exist');
                }

                $this->caller_class::dispatch_event('beforeExecute', $this->getArgs(), 4);

                $this->stmn = $this->caller_class::dispatch_filter('stmn', $this->stmn, $this->getArgs(), 4);
                $method = $this->caller_class::dispatch_filter('method', $method, $this->getArgs(), 4);

                try {
                    $values = $this->stmn->execute();

                    if (in_array($method, ['fetch', 'fetchAll'])) {
                        $values = $this->stmn->$method();
                    }
                } catch (\Exception $e) {
                    // Ensure cursor is closed even on exceptions
                    if (isset($this->stmn)) {
                        $this->stmn->closeCursor();
                    }
                    throw $e;
                } finally {
                    // Always ensure proper cleanup
                    if (isset($this->stmn)) {
                        $this->stmn->closeCursor();
                    }
                }

                $this->caller_class::dispatch_event('afterExecute', $this->getArgs(), 4);

                return $this->caller_class::dispatch_filter('return', $values, $this->getArgs(), 4);
            }

            /**
             * Destructor to ensure proper cleanup of database resources
             */
            public function __destruct()
            {
                if (isset($this->stmn)) {
                    try {
                        $this->stmn->closeCursor();
                    } catch (\Exception $e) {
                        // Silently handle any cleanup errors
                    }
                }
            }
        };
    }

    /**
     * patch - updates a record in the database
     *
     * @param  int  $id  - the id of the record to update
     * @param  array  $params  - the parameters to update
     */
    public function patch(int $id, array $params): bool
    {

        unset($params['act']);

        if ($this->entity == '') {
            report('Patch not implemented for this entity');

            return false;
        }

        $sql = 'UPDATE zp_'.$this->entity.' SET ';

        foreach ($params as $key => $value) {
            $sql .= ''.Db::sanitizeToColumnString($key).'=:'.Db::sanitizeToColumnString($key).', ';
        }

        $sql .= 'id=:id WHERE id=:id LIMIT 1';

        $call = $this->dbcall(func_get_args());

        $call->prepare($sql);

        $call->bindValue(':id', $id, PDO::PARAM_STR);

        foreach ($params as $key => $value) {
            $call->bindValue(':'.Db::sanitizeToColumnString($key), $value, PDO::PARAM_STR);
        }

        return $call->execute();
    }

    /**
     * @throws \ReflectionException
     */
    public function insert(object $objectToInsert): false|int
    {

        if ($this->entity == '') {
            report('Insert not implemented for this entity');

            return false;
        }

        $sql = 'INSERT INTO zp_'.$this->entity.' (';

        $sqlArr = [];
        foreach ($objectToInsert as $key => $value) {
            if ($this->getFieldAttribute($objectToInsert, $key)) {
                $sqlArr[] = '`'.Db::sanitizeToColumnString($key).'`';
            }
        }
        $sql .= implode(',', $sqlArr);

        $sql .= ') VALUES (';

        $sqlArr2 = [];
        foreach ($objectToInsert as $key => $value) {
            if ($this->getFieldAttribute($objectToInsert, $key)) {
                $sqlArr2[] = ':'.Db::sanitizeToColumnString($key).'';
            }
        }
        $sql .= implode(',', $sqlArr2);

        $sql .= ')';

        $call = $this->dbcall(func_get_args());

        $call->prepare($sql);

        foreach ($objectToInsert as $key => $value) {
            if ($this->getFieldAttribute($objectToInsert, $key)) {
                $call->bindValue(':'.Db::sanitizeToColumnString($key), $value, PDO::PARAM_STR);
            }
        }

        $call->execute();

        return $call->lastInsertId();
    }

    /**
     * delete - deletes a record from the database
     *
     * @param  int  $id  - the id of the record to delete
     */
    public function delete(int $id): void {}

    /**
     * get - gets a record from the database
     *
     * @param  int  $id  - the id of the record to get
     *
     * @throws BindingResolutionException
     * @throws \ReflectionException
     */
    public function get(int $id): mixed
    {
        if ($this->entity == '' || $this->model == '') {
            report('Get not implemented for this entity');

            return false;
        }

        $sql = 'SELECT ';

        $entityModel = app()->make($this->model);
        $dbFields = $this->getDbFields($this->model);

        $sql .= implode(',', $dbFields);

        $sql .= ' FROM zp_'.$this->entity.' WHERE id = :id ';

        $call = $this->dbcall(func_get_args());

        $call->prepare($sql);

        $call->bindValue(':id', $id, PDO::PARAM_STR);

        $call->execute();

        $call->setFetchMode(PDO::FETCH_CLASS, $this->model);

        return $call->fetch();
    }

    /**
     * getFieldAttribute - gets the field attribute for a given property
     *
     * @param  object|string  $class  - the class to get the attribute from
     * @param  string  $property  - the property to get the attribute from
     * @param  bool  $includeId  - whether or not to include the id attribute
     *
     * @throws \ReflectionException
     */
    protected function getFieldAttribute(object|string $class, string $property, bool $includeId = false): array|false
    {
        // Don't create or update id attributes
        if ($includeId === false && $property == 'id') {
            return false;
        }

        $property = new ReflectionProperty($class, $property);

        $attributes = $property->getAttributes();
        foreach ($attributes as $attribute) {
            $name = $attribute->getName();
            if (str_contains($name, 'DbColumn')) {
                return $attribute->getArguments();
            }
        }

        return false;
    }

    /**
     * getDbFields - gets the database fields for a given class
     *
     * @param  object|string  $class  - the class to get the fields from
     *
     * @throws \ReflectionException
     */
    protected function getDbFields(object|string $class): array
    {
        $property = new ReflectionClass($class);

        $properties = $property->getProperties();

        $propertyArray = [];
        foreach ($properties as $property) {
            if ($this->getFieldAttribute($class, $property->getName(), true)) {
                $propertyArray[] = $property->getName();
            }
        }

        return $propertyArray;
    }
}
