<?php

namespace app\service;

use Exception;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\Model;
use think\facade\Db;
use think\Collection;
use think\facade\Env;
use think\facade\Log;
use think\Paginator;
use app\exception\RepositoryException;

/**
 * 领域模型-仓储
 *
 * Class Repository
 * @package app\service
 */
class Repository
{
    /**
     * @var array 已加载对象列表
     */
    private static $objects = [];

    /**
     * @var Model 模型对象
     */
    protected $model;

    // 布尔值数字关系
    public const BOOL_FALSE = 0;
    public const BOOL_TRUE  = 1;

    /**
     * 获取当前子对象实列(单例形式返回)
     *
     * @param  Model|null  $model  模型对象。未指定则自动获取
     * @return mixed
     */
    public static function getInstance(Model $model = null)
    {
        $class = get_called_class();

        if (isset(self::$objects[$class]) && self::$objects[$class] !== null) {
            return self::$objects[$class];
        }

        $obj = new $class();
        if ($model) {
            $obj->model = $model;
        } elseif (strpos($class, '\\repository') > 0) {
            $model = str_replace('\\repository', '\\model', $class);
            //去掉末尾Repository app\model\AccountRepository  =》 app\model\Account
            $model = substr($model, 0, strlen($model) - strlen('Repository'));
            if (class_exists($model)) {
                $obj->model = new $model;
            }
        }

        self::$objects[$class] = $obj;
        return $obj;
    }

    /**
     * @param  callable  $callback
     * @param  mixed  $failReturn
     * @param  bool  $transaction
     * @return mixed
     * @throws RepositoryException
     */
    protected function access(callable $callback, $failReturn, bool $transaction = false)
    {
        $exception = null;
        try {
            if ($transaction) {
                Db::startTrans();
            }

            $r = $callback();

            if ($transaction) {
                Db::commit();
            }

            if ($r) {
                return $r;
            }

            if ($failReturn instanceof Exception) {
                return null;
            }

            return $failReturn;
        } catch (Exception $e) {
            if ($transaction) {
                Db::rollback();
            }

            if ($e instanceof RepositoryException) {
                throw $e;
            }

            $name = 'Domain - Repository - 未知错误';

            $traces = $e->getTrace();

            foreach ($traces as $i => $trace) {
                if (!empty($trace['class']) && $trace['class'] === Repository::class && $trace['function'] === 'access') {
                    $trace = $traces[$i - 2] ?? null;
                    break;
                }
            }

            if (!empty($trace) && !empty($trace['file'][1]) && !empty($trace['line'])) {
                $parts = explode('application\\', $trace['file']);
                if (!empty($parts[1])) {
                    $name = $parts[1].':'.$trace['line'];
                } else {
                    $name = $trace['file'].':'.$trace['line'];
                }
            }

            $exception = $e;

            $line = '['.$name.'] '.$e->getMessage();
            Log::error($line);

            throw new RepositoryException('Repository异常'.$line.(Env::get('app_debug') ? ': '.$line : ''), 5009);
        } finally {
            if ($exception && $failReturn instanceof RepositoryException) {
                if (Env::get('app_debug')) {
                    $failReturn = new RepositoryException($failReturn->getMessage().': '.$exception->getMessage(),
                        $failReturn->getCode());
                }
                throw $failReturn;
            }
        }
    }

    /**
     * 获取当前model对象
     *
     * @return Model
     */
    public function getModel(): Model
    {
        return $this->model;
    }

    /**
     * 根据条件查询列表
     *
     * @param  array  $where  查询条件
     * @param  array  $fields  查询字段 []表示全部
     * @param  int  $page  默认第一页 0不限制
     * @param  int  $limit  限制条数 0不限制
     * @param  callable|null  $callback 更为复杂的条件 使用闭包查询
     * @return array
     * @throws RepositoryException
     */
    public function findList(array $where = [], array $fields = [], int $page = 1, int $limit = 0, callable $callback = null, array $order = []): ?array
    {
        $failData = [
            'total'   => 0,
            'current' => $page,
            'size'    => $limit,
            'list'    => new Collection(),
        ];
        return $this->access(function () use ($where, $fields, $page, $limit, $callback, $order) {
            return $this->model->findList($where, $fields, $page, $limit, $callback, $order);
        }, $failData);
    }

    /**
     * 根据条件查询列表[带分页 适用于后台]
     *
     * @param  array  $data  查询数据
     * @param  array  $pageParams  分页参数
     * @param  callable|null  $callback 复杂查询条件  使用闭包查询
     * @return Paginator
     */
    public function findListWithPaginate(array $data = [], array $pageParams = [], callable $callback = null): Paginator
    {
        return $this->model->findListWithPaginate($data, $pageParams, $callback);
    }

    /**
     * 根据主键 ID 查询
     *
     * @param  int  $id  ID
     * @param  array  $fields  要返回的字段,默认全部
     * @return Mixed
     * @throws RepositoryException
     */
    public function findById(int $id, array $fields = [], callable $callback = null)
    {
        return $this->access(function () use ($id, $fields, $callback) {
            return $this->model->findById($id, $fields, $callback);
        }, null);
    }

    /**
     * @param  array  $where
     * @param  array  $fields
     * @return array|Model|null
     * @throws DataNotFoundException
     * @throws DbException
     * @throws ModelNotFoundException
     */
    public function findOneByWhere(array $where, array $fields = [])
    {
        return $this->model->field($fields)->where($where)->find();
    }

    /**
     * 创建
     *
     * @param  array  $data  数据
     * @return Model
     * @throws RepositoryException
     */
    public function create(array $data): Model
    {
        return $this->access(function () use ($data) {
            return $this->model->create($data);
        }, new RepositoryException('创建失败'));
    }

    /**
     * 更新
     *
     * @param  array  $data  数据
     * @param  array  $where  条件
     * @return bool|Exception
     * @throws RepositoryException
     */
    public function update(array $data, array $where): bool
    {
        return $this->access(function () use ($data, $where) {
            return $this->model->where($where)->find()->save($data);
        }, new RepositoryException('更新失败'));
    }

    /**
     * 删除
     *
     * @param  array  $where  删除条件
     * @param  bool  $softDelete  是否软删除 默认false
     * @param  string  $softDeleteTime  删除时间 softDelete=true时有效
     * @param  string  $softDeleteField  软删除字段 softDelete=true时有效
     * @return bool
     * @throws RepositoryException
     */
    public function delete(array $where, bool $softDelete = false, string $softDeleteTime = '', string $softDeleteField = 'deleted_at'): bool
    {
        return $this->access(function () use ($where, $softDelete, $softDeleteField, $softDeleteTime) {
            // 注意:如果model中引入了软删除trait,$softDelete又设置false 将无法正确删除
            return $this->model->where($where)
                ->when($softDelete, function ($q) use ($softDeleteField, $softDeleteTime) {
                    $softDeleteTime = $softDeleteTime ?: date('Y-m-d H:i:s');
                    $q->useSoftDelete($softDeleteField, $softDeleteTime);
                })->delete();
        }, false);
    }

    /**
     * 排序
     *
     * @param  int  $id 排序ID
     * @param  string  $type 排序类型 向上、向下
     * @param  int  $num 移动位数
     * @param  string  $listType 列表的排序类型  降序|升序
     * @param  array  $where 额外条件 格式如:
     *                                  $map[] = ['name','like','think'];
     *                                  $map[] = ['status','=',1];
     * @return array
     */
    public function sort(int $id,string $type,int $num,string $listType, array $where = []): array
    {
        return $this->model->sort($id, $type, $num, $listType, $where);
    }

    /**
     * 日志记录
     *
     * @param  string  $msg
     * @param  Exception|null  $e
     * @param  string  $level
     * @param  string  $channel
     */
    public static function log(string $msg, Exception $e = null, string $level = 'error', string $channel = 'file')
    {
        if ($e != null) {
            $msg = sprintf("[%s]%s:%s %s", $msg, $e->getFile(), $e->getLine(), $e->getMessage());
        } else {
            $msg = sprintf("%s", $msg);

        }
        Log::channel($channel)->$level($msg);
    }
}