<?php
/**
 * Laravel-like Eloquent Model Base Class
 *
 * @package ZenoChatbot\Framework\Database
 */

namespace ZenoChatbot\Framework\Database;

use ZenoChatbot\Framework\Support\Collection;
use ZenoChatbot\Framework\Database\ORM\Connection;
use ZenoChatbot\Framework\Support\Arr;
use ZenoChatbot\Framework\Database\Relations\HasOne;
use ZenoChatbot\Framework\Database\Relations\HasMany;
use ZenoChatbot\Framework\Database\Relations\BelongsTo;
use ZenoChatbot\Framework\Database\Relations\BelongsToMany;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Abstract Model Base Class
 */
abstract class Model implements \JsonSerializable
{
    /**
     * The database query builder
     *
     * @var \ZenoChatbot\Framework\Database\QueryBuilder
     */
    protected static $connection;

    /**
     * The table associated with the model
     *
     * @var string
     */
    protected $table;

    /**
     * The primary key for the model
     *
     * @var string
     */
    protected $primaryKey = 'id';

    /**
     * The attributes that are mass assignable
     *
     * @var array
     */
    protected $fillable = [];

    /**
     * The attributes that should be hidden for arrays
     *
     * @var array
     */
    protected $hidden = [];

    /**
     * The attributes that should be cast to native types
     *
     * @var array
     */
    protected $casts = [];

    /**
     * Indicates if the model should be timestamped
     *
     * @var bool
     */
    public $timestamps = true;

    /**
     * The name of the "created at" column
     *
     * @var string
     */
    const CREATED_AT = 'created_at';

    /**
     * The name of the "updated at" column
     *
     * @var string
     */
    const UPDATED_AT = 'updated_at';

    /**
     * The model's attributes
     *
     * @var array
     */
    protected $attributes = [];

    /**
     * The model's original attributes
     *
     * @var array
     */
    protected $original = [];

    /**
     * Indicates if the model exists
     *
     * @var bool
     */
    public $exists = false;

    /**
     * The loaded relationships for the model
     *
     * @var array
     */
    protected $relations = [];



    /**
     * Create a new model instance
     *
     * @param array $attributes
     */
    public function __construct(array $attributes = [])
    {
        $this->fill($attributes);
        $this->syncOriginal();
    }

    /**
     * Get the database query builder
     *
     * @return QueryBuilder
     */
    public static function getConnection()
    {
        return static::$connection;
    }

    /**
     * Get the table name
     *
     * @return string
     */
    public function getTable()
    {
        if (!isset($this->table)) {
            $className = class_basename(static::class);
            $this->table = strtolower($className) . 's';
        }

        return $this->table;
    }

    /**
     * Get the primary key name
     *
     * @return string
     */
    public function getKeyName()
    {
        return $this->primaryKey;
    }

    /**
     * Get the primary key value
     *
     * @return mixed
     */
    public function getKey()
    {
        return $this->getAttribute($this->getKeyName());
    }

    /**
     * Create a new query builder for the model
     *
     * @return QueryBuilder
     */
    public function newQuery()
    {
        // Create a fresh QueryBuilder instance to avoid state pollution
        $query = new QueryBuilder();
        $query->table($this->getTable());
        $query->setModel(new static);
        return $query;
    }

    /**
     * Create a new query builder for the model (static)
     *
     * @return QueryBuilder
     */
    public static function query()
    {
        return (new static)->newQuery();
    }

    /**
     * Find a model by its primary key
     *
     * @param mixed $id
     * @return static|null
     */
    public static function find($id)
    {
        $instance = new static;
        $result = $instance->newQuery()->find($id, $instance->getKeyName());

        if ($result) {
            // If result is already a Model instance, return it directly
            if ($result instanceof static) {
                return $result;
            }
            // If result is an array, convert to Model instance
            return $instance->newFromBuilder($result);
        }

        return null;
    }

    /**
     * Find multiple models by their primary keys
     *
     * @param array $ids
     * @return Collection
     */
    public static function findMany(array $ids)
    {
        $instance = new static;
        $results = $instance->newQuery()
            ->whereIn($instance->getKeyName(), $ids)
            ->get();

        return $results->map(function ($result) use ($instance) {
            return $instance->newFromBuilder($result);
        });
    }

    /**
     * Find a model by its primary key or throw an exception
     *
     * @param mixed $id
     * @return static
     * @throws \RuntimeException
     */
    public static function findOrFail($id)
    {
        $result = static::find($id);

        if (!$result) {
            throw new \RuntimeException("Model not found with ID: {$id}");
        }

        return $result;
    }

    /**
     * Get all models
     *
     * @return Collection
     */
    public static function all()
    {
        $instance = new static;
        $results = $instance->newQuery()->get();

        return $results->map(function ($result) use ($instance) {
            // If result is already a Model instance, return it as-is
            if ($result instanceof Model) {
                return $result;
            }
            // Otherwise, convert array to Model instance
            return $instance->newFromBuilder($result);
        });
    }

    /**
     * Paginate the given query using ORM-bound paginator
     *
     * @param int|null $perPage
     * @param int|null $page
     * @param array $options
     * @return \ZenoChatbot\Framework\Database\LengthAwarePaginator
     */
    public static function paginate($perPage = null, $page = null, array $options = [])
    {
        $instance = new static;
        $paginator = $instance->newQuery()->paginate($perPage, $page, $options);

        // Transform the items to model instances
        $paginator->through(function ($item) use ($instance) {
            // If item is already a Model instance, return it as-is
            if ($item instanceof Model) {
                return $item;
            }
            // Otherwise, convert array to Model instance
            return $instance->newFromBuilder($item);
        });

        return $paginator;
    }



    /**
     * Create a new model instance
     *
     * @param array $attributes
     * @return static
     */
    public static function create(array $attributes = [])
    {
        $model = new static($attributes);
        $model->save();

        return $model;
    }

    /**
     * Update or create a model
     *
     * @param array $attributes
     * @param array $values
     * @return static
     */
    public static function updateOrCreate(array $attributes, array $values = [])
    {
        $instance = new static;
        $query = $instance->newQuery();

        foreach ($attributes as $key => $value) {
            $query->where($key, $value);
        }

        $result = $query->first();

        if ($result) {
            $model = $instance->newFromBuilder($result);
            $model->fill($values);
            $model->save();
            return $model;
        }

        return static::create(array_merge($attributes, $values));
    }

    /**
     * Update the model with the given attributes
     *
     * @param array $attributes
     * @return bool
     */
    public function update(array $attributes = [])
    {
        if (!$this->exists) {
            return false;
        }

        $this->fill($attributes);
        return $this->save();
    }

    /**
     * Save the model to the database
     *
     * @return bool
     */
    public function save()
    {
        if ($this->exists) {
            return $this->performUpdate();
        }

        return $this->performInsert();
    }

    /**
     * Delete the model from the database
     *
     * @return bool
     */
    public function delete()
    {
        if (!$this->exists) {
            return false;
        }

        $result = $this->newQuery()
            ->where($this->getKeyName(), $this->getKey())
            ->delete();

        if ($result) {
            $this->exists = false;
            return true;
        }

        return false;
    }

    /**
     * Fill the model with an array of attributes
     *
     * @param array $attributes
     * @return $this
     */
    public function fill(array $attributes)
    {
        foreach ($attributes as $key => $value) {
            if ($this->isFillable($key)) {
                $this->setAttribute($key, $value);
            }
        }

        return $this;
    }

    /**
     * Get an attribute from the model
     *
     * @param string $key
     * @return mixed
     */
    public function getAttribute($key)
    {
        if (!$key) {
            return null;
        }

        $value = Arr::get($this->attributes, $key);

        // Apply casting
        if (isset($this->casts[$key])) {
            return $this->castAttribute($key, $value);
        }

        return $value;
    }

    /**
     * Set an attribute on the model
     *
     * @param string $key
     * @param mixed $value
     * @return $this
     */
    public function setAttribute($key, $value)
    {
        $this->attributes[$key] = $value;
        return $this;
    }

    /**
     * Determine if the given attribute is fillable
     *
     * @param string $key
     * @return bool
     */
    public function isFillable($key)
    {
        if (empty($this->fillable)) {
            return true;
        }

        return in_array($key, $this->fillable);
    }

    /**
     * Get all attributes
     *
     * @return array
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * Convert the model to an array
     *
     * @return array
     */
    public function toArray()
    {
        $attributes = $this->attributes;

        // Remove hidden attributes
        foreach ($this->hidden as $hidden) {
            unset($attributes[$hidden]);
        }

        // Apply casting
        foreach ($attributes as $key => $value) {
            if (isset($this->casts[$key])) {
                $attributes[$key] = $this->castAttribute($key, $value);
            }
        }

        // Add relationships to the array
        foreach ($this->relations as $key => $value) {
            if ($value instanceof Collection) {
                $attributes[$key] = $value->map(function($item) {
                    return $item instanceof Model ? $item->toArray() : $item;
                })->toArray();
            } elseif ($value instanceof Model) {
                $attributes[$key] = $value->toArray();
            } else {
                $attributes[$key] = $value;
            }
        }

        return $attributes;
    }

    /**
     * Convert the model to JSON
     *
     * @param int $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this->jsonSerialize(), $options);
    }

    /**
     * Create a new model instance from builder result
     *
     * @param array|Model $attributes
     * @return static
     */
    public function newFromBuilder($attributes)
    {
        // Safety check - if somehow a Model instance is passed, return it
        if ($attributes instanceof Model) {
            return $attributes;
        }

        // Ensure we have an array
        if (!is_array($attributes)) {
            throw new \InvalidArgumentException('Attributes must be an array or Model instance');
        }

        $model = new static;

        // Separate relationships from regular attributes
        $modelAttributes = [];
        $relations = [];

        foreach ($attributes as $key => $value) {
            if (method_exists($model, $key) && $model->isRelationshipMethod($key)) {
                $relations[$key] = $value;
            } else {
                $modelAttributes[$key] = $value;
            }
        }

        $model->setRawAttributes($modelAttributes, true);
        $model->exists = true;

        // Set relationships - convert Collection to array for JSON serialization
        foreach ($relations as $relationName => $relationValue) {
            if ($relationValue instanceof \ZenoChatbot\Framework\Support\Collection) {
                // Convert Collection of arrays to Collection of Models
                $relationValue = $relationValue->map(function($item) use ($relationName) {
                    if (is_array($item)) {
                        // Get the related model class
                        $relationMethod = $this->$relationName();
                        $relatedModel = $relationMethod->getModel();
                        return $relatedModel->newFromBuilder($item);
                    }
                    return $item;
                });
            }
            $model->setRelation($relationName, $relationValue);
        }

        return $model;
    }

    /**
     * Create a fresh model instance with the given attributes.
     * This method is used by relationship create() helpers to avoid
     * routing through the QueryBuilder via __call.
     *
     * @param array $attributes
     * @param bool $exists
     * @return static
     */
    public function newInstance(array $attributes = [], $exists = false)
    {
        $model = new static;
        $model->exists = $exists;

        if (!empty($attributes)) {
            $model->fill($attributes);
        }

        return $model;
    }

    /**
     * Check if a method is a relationship method
     *
     * @param string $method
     * @return bool
     */
    protected function isRelationshipMethod($method)
    {
        if (!method_exists($this, $method)) {
            return false;
        }

        try {
            $result = $this->$method();
            return $result instanceof \ZenoChatbot\Framework\Database\Relations\Relation;
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Set the raw attributes array
     *
     * @param array $attributes
     * @param bool $sync
     * @return $this
     */
    public function setRawAttributes(array $attributes, $sync = false)
    {
        $this->attributes = $attributes;

        if ($sync) {
            $this->syncOriginal();
        }

        return $this;
    }

    /**
     * Sync the original attributes with the current
     *
     * @return $this
     */
    public function syncOriginal()
    {
        $this->original = $this->attributes;
        return $this;
    }

    /**
     * Perform the actual insert operation
     *
     * @return bool
     */
    protected function performInsert()
    {
        $attributes = $this->attributes;

        // Add timestamps
        if ($this->timestamps) {
            $now = current_time('mysql');
            $attributes[static::CREATED_AT] = $now;
            $attributes[static::UPDATED_AT] = $now;
        }

        $id = $this->newQuery()->insert($attributes);

        if ($id) {
            $this->setAttribute($this->getKeyName(), $id);
            $this->exists = true;
            $this->syncOriginal();
            return true;
        }

        return false;
    }

    /**
     * Perform the actual update operation
     *
     * @return bool
     */
    protected function performUpdate()
    {
        $dirty = $this->getDirty();

        if (empty($dirty)) {
            return true;
        }

        // Add updated timestamp
        if ($this->timestamps) {
            $dirty[static::UPDATED_AT] = current_time('mysql');
        }

        $result = $this->newQuery()
            ->where($this->getKeyName(), $this->getKey())
            ->update($dirty);

        if ($result !== false) {
            $this->syncOriginal();
            return true;
        }

        return false;
    }

    /**
     * Get the attributes that have been changed since last sync
     *
     * @return array
     */
    public function getDirty()
    {
        $dirty = [];

        foreach ($this->attributes as $key => $value) {
            if (!array_key_exists($key, $this->original) || $this->original[$key] !== $value) {
                $dirty[$key] = $value;
            }
        }

        return $dirty;
    }

    /**
     * Determine if the model or given attribute(s) have been modified
     *
     * @param array|string|null $attributes
     * @return bool
     */
    public function isDirty($attributes = null)
    {
        $dirty = $this->getDirty();

        if (is_null($attributes)) {
            return !empty($dirty);
        }

        $attributes = is_array($attributes) ? $attributes : func_get_args();

        foreach ($attributes as $attribute) {
            if (array_key_exists($attribute, $dirty)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Cast an attribute to a native PHP type
     *
     * @param string $key
     * @param mixed $value
     * @return mixed
     */
    protected function castAttribute($key, $value)
    {
        if (is_null($value)) {
            return $value;
        }

        $castType = $this->casts[$key];

        switch ($castType) {
            case 'int':
            case 'integer':
                return (int) $value;
            case 'real':
            case 'float':
            case 'double':
                return (float) $value;
            case 'string':
                return (string) $value;
            case 'bool':
            case 'boolean':
                return (bool) $value;
            case 'array':
            case 'json':
                return is_string($value) ? json_decode($value, true) : $value;
            case 'object':
                return is_string($value) ? json_decode($value) : $value;
            case 'datetime':
                return is_string($value) ? new \DateTime($value) : $value;
            default:
                return $value;
        }
    }

    /**
     * Get the class basename
     *
     * @param string $class
     * @return string
     */
    protected static function class_basename($class)
    {
        $class = is_object($class) ? get_class($class) : $class;
        return basename(str_replace('\\', '/', $class));
    }

    /**
     * Handle dynamic method calls
     *
     * @param string $method
     * @param array $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        // Delegate to query builder
        return $this->newQuery()->$method(...$parameters);
    }

    /**
     * Handle dynamic static method calls
     *
     * @param string $method
     * @param array $parameters
     * @return mixed
     */
    public static function __callStatic($method, $parameters)
    {
        return (new static)->$method(...$parameters);
    }

    /**
     * Dynamically retrieve attributes on the model
     *
     * @param string $key
     * @return mixed
     */
    public function __get($key)
    {
        // Check if it's a loaded relation
        if (array_key_exists($key, $this->relations)) {
            return $this->relations[$key];
        }

        // Check if it's a relationship method
        if (method_exists($this, $key)) {
            return $this->getRelationshipFromMethod($key);
        }

        return $this->getAttribute($key);
    }

    /**
     * Dynamically set attributes on the model
     *
     * @param string $key
     * @param mixed $value
     * @return void
     */
    public function __set($key, $value)
    {
        $this->setAttribute($key, $value);
    }

    /**
     * Determine if an attribute exists on the model
     *
     * @param string $key
     * @return bool
     */
    public function __isset($key)
    {
        return !is_null($this->getAttribute($key));
    }

    /**
     * Unset an attribute on the model
     *
     * @param string $key
     * @return void
     */
    public function __unset($key)
    {
        unset($this->attributes[$key]);
    }

    /**
     * Convert the model instance to its JSON representation
     *
     * @return array
     */
    public function jsonSerialize(): array
    {
        return $this->toArray();
    }

    /**
     * Convert the model instance to JSON
     *
     * @return string
     */
    public function __toString()
    {
        return $this->toJson();
    }

    /**
     * Define a one-to-one relationship
     *
     * @param string $related
     * @param string $foreignKey
     * @param string $localKey
     * @return HasOne
     */
    protected function hasOne($related, $foreignKey = null, $localKey = null)
    {
        $instance = new $related;

        $foreignKey = $foreignKey ?: $this->getForeignKey();
        $localKey = $localKey ?: $this->getKeyName();

        return new HasOne($this, $instance, $foreignKey, $localKey);
    }

    /**
     * Define a one-to-many relationship
     *
     * @param string $related
     * @param string $foreignKey
     * @param string $localKey
     * @return HasMany
     */
    protected function hasMany($related, $foreignKey = null, $localKey = null)
    {
        $instance = new $related;

        $foreignKey = $foreignKey ?: $this->getForeignKey();
        $localKey = $localKey ?: $this->getKeyName();

        return new HasMany($this, $instance, $foreignKey, $localKey);
    }

    /**
     * Define an inverse one-to-one or many relationship
     *
     * @param string $related
     * @param string $foreignKey
     * @param string $ownerKey
     * @param string $relation
     * @return BelongsTo
     */
    protected function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
    {
        if (is_null($relation)) {
            $relation = $this->guessBelongsToRelation();
        }

        $instance = new $related;

        if (is_null($foreignKey)) {
            $foreignKey = $this->snake_case($relation) . '_' . $instance->getKeyName();
        }

        $ownerKey = $ownerKey ?: $instance->getKeyName();

        return new BelongsTo($this, $instance, $foreignKey, $ownerKey, $relation);
    }

    /**
     * Define a many-to-many relationship
     *
     * @param string $related
     * @param string $table
     * @param string $foreignPivotKey
     * @param string $relatedPivotKey
     * @param string $parentKey
     * @param string $relatedKey
     * @return BelongsToMany
     */
    protected function belongsToMany(
        $related,
        $table = null,
        $foreignPivotKey = null,
        $relatedPivotKey = null,
        $parentKey = null,
        $relatedKey = null
    ) {
        $instance = new $related;

        $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
        $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();

        if (is_null($table)) {
            $table = $this->joiningTable($related);
        }

        return new BelongsToMany(
            $this, $instance, $table, $foreignPivotKey,
            $relatedPivotKey, $parentKey ?: $this->getKeyName(),
            $relatedKey ?: $instance->getKeyName()
        );
    }

    /**
     * Get the default foreign key name for the model
     *
     * @return string
     */
    public function getForeignKey()
    {
        return $this->snake_case(class_basename($this)) . '_' . $this->getKeyName();
    }

    /**
     * Get the joining table name for a many-to-many relation
     *
     * @param string $related
     * @return string
     */
    public function joiningTable($related)
    {
        $models = [
            $this->snake_case(class_basename($this)),
            $this->snake_case(class_basename($related)),
        ];

        sort($models);

        return strtolower(implode('_', $models));
    }

    /**
     * Guess the "belongs to" relationship name
     *
     * @return string
     */
    protected function guessBelongsToRelation()
    {
        list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);

        return $caller['function'];
    }

    /**
     * Get a relationship value from a method
     *
     * @param string $method
     * @return mixed
     */
    protected function getRelationshipFromMethod($method)
    {
        $relation = $this->$method();

        if (!$relation instanceof \ZenoChatbot\Framework\Database\Relations\Relation) {
            throw new \LogicException(sprintf(
                '%s::%s must return a relationship instance.',
                static::class, $method
            ));
        }

        return $this->tap($relation->getResults(), function ($results) use ($method) {
            $this->setRelation($method, $results);
        });
    }

    /**
     * Get a relationship
     *
     * @param string $key
     * @return mixed
     */
    public function getRelation($key)
    {
        return $this->relations[$key] ?? null;
    }

    /**
     * Set a relationship
     *
     * @param string $relation
     * @param mixed $value
     * @return $this
     */
    public function setRelation($relation, $value)
    {
        $this->relations[$relation] = $value;

        return $this;
    }

    /**
     * Unset a relationship
     *
     * @param string $relation
     * @return $this
     */
    public function unsetRelation($relation)
    {
        unset($this->relations[$relation]);

        return $this;
    }

    /**
     * Get all the loaded relations for the instance
     *
     * @return array
     */
    public function getRelations()
    {
        return $this->relations;
    }

    /**
     * Set the entire relations array on the model
     *
     * @param array $relations
     * @return $this
     */
    public function setRelations(array $relations)
    {
        $this->relations = $relations;

        return $this;
    }

    /**
     * Determine if the given relation is loaded
     *
     * @param string $key
     * @return bool
     */
    public function relationLoaded($key)
    {
        return array_key_exists($key, $this->relations);
    }

    /**
     * Helper function for snake_case conversion
     *
     * @param string $value
     * @return string
     */
    protected function snake_case($value)
    {
        return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $value));
    }

    /**
     * Set the database query builder for all models
     *
     * @param \ZenoChatbot\Framework\Database\QueryBuilder $connection
     * @return void
     */
    public static function setConnection($connection)
    {
        static::$connection = $connection;
    }



    /**
     * Helper function for tap
     *
     * @param mixed $value
     * @param callable $callback
     * @return mixed
     */
    protected function tap($value, callable $callback)
    {
        $callback($value);
        return $value;
    }
}
