<?php

namespace Ignite\Core\Audit;

use Ignite\Core\Entities\Audit;
use Ignite\Core\Entities\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;

class Auditor
{
    /**
     * Save an audit log.
     *
     * @param Model $model
     * @param Model $context
     * @param string $type
     * @param string $level
     * @param string|null $description
     * @param User|Authenticatable|null $user
     *
     * @return Model
     */
    public function audit($model, $context, $type, $level = 'info', $description = null, $user = null)
    {
        $user = $user ?? auth()->user();

        list($old, $new) = $this->getEventAttributes($model, $type);

        $data = [
            'user_id'          => $user->getKey(),
            'user_type'        => get_class($user),
            'context_id'       => $context->getKey(),
            'context_type'     => get_class($context),
            'event'            => $type,
            'auditable_id'     => $model->getKey(),
            'auditable_type'   => get_class($model),
            'url'              => Request::url(),
            'ip_address'       => Request::ip(),
            'user_agent'       => Request::header('User-Agent'),
            'created_at'       => $now = now()->format('Y-m-d H:i:s'),
            'updated_at'       => $now,
            'level'            => $level,
            'description'      => $description,
            'tags'             => null,
            'old_values'       => $old,
            'new_values'       => $new,
        ];

        return Audit::query()->create($data);
    }

    /**
     * Save a create audit log.
     *
     * @param Model $model
     * @param string $level
     * @param string|null $description
     * @param User|Authenticatable|null $user
     *
     * @return Model
     */
    public function create($model, $level = 'info', $description = null, $user = null)
    {
        return $this->audit($model, 'created', $level, $description, $user);
    }

    /**
     * Save an update audit log.
     *
     * @param Model $model
     * @param string $level
     * @param string|null $description
     * @param User|Authenticatable|null $user
     *
     * @return Model
     */
    public function update($model, $level = 'info', $description = null, $user = null)
    {
        return $this->audit($model, 'updated', $level, $description, $user);
    }

    /**
     * Save a delete audit log.
     *
     * @param Model $model
     * @param string $level
     * @param string|null $description
     * @param User|Authenticatable|null $user
     *
     * @return Model
     */
    public function delete($model, $level = 'info', $description = null, $user = null)
    {
        return $this->audit($model, 'deleted', $level, $description, $user);
    }

    /**
     * Save a restored audit log.
     *
     * @param Model $model
     * @param string $level
     * @param string|null $description
     * @param User|Authenticatable|null $user
     *
     * @return Model
     */
    public function restore($model, $level = 'info', $description = null, $user = null)
    {
        return $this->audit($model, 'restored', $level, $description, $user);
    }

    /**
     * Get the old/new attributes for the given event.
     *
     * @param Model $model
     * @param string $type
     *
     * @return array[]
     */
    protected function getEventAttributes(Model $model, string $type)
    {
        $method = 'get' . Str::studly($type) . 'EventAttributes';

        if (! method_exists($this, $method)) {
            return [
                [],
                [],
            ];
        }

        return $this->$method($model);
    }

    /**
     * Get the old/new attributes of a created event.
     *
     * @param Model $model
     *
     * @return array
     */
    protected function getCreatedEventAttributes(Model $model): array
    {
        $new = [];

        foreach ($model->getAttributes() as $attribute => $value) {
            if ($this->isAttributeAuditable($model, $attribute)) {
                $new[$attribute] = $value;
            }
        }

        return [
            [],
            $new,
        ];
    }

    /**
     * Get the old/new attributes of an updated event.
     *
     * @param Model $model
     *
     * @return array
     */
    protected function getUpdatedEventAttributes(Model $model): array
    {
        $old = [];
        $new = [];

        foreach ($model->getDirty() as $attribute => $value) {
            if ($this->isAttributeAuditable($model, $attribute)) {
                $old[$attribute] = Arr::get($model->getOriginal(), $attribute);
                $new[$attribute] = Arr::get($model->getAttributes(), $attribute);
            }
        }

        return [
            $old,
            $new,
        ];
    }

    /**
     * Get the old/new attributes of a deleted event.
     *
     * @param Model $model
     *
     * @return array
     */
    protected function getDeletedEventAttributes(Model $model): array
    {
        $old = [];

        foreach ($model->getAttributes() as $attribute => $value) {
            if ($this->isAttributeAuditable($model, $attribute)) {
                $old[$attribute] = $value;
            }
        }

        return [
            $old,
            [],
        ];
    }

    /**
     * Get the old/new attributes of a restored event.
     *
     * @param Model $model
     *
     * @return array
     */
    protected function getRestoredEventAttributes(Model $model): array
    {
        // A restored event is just a deleted event in reverse
        return array_reverse($this->getDeletedEventAttributes($model));
    }

    /**
     * Check if the attribute is auditable.
     *
     * @param Model $model
     * @param string $attribute
     *
     * @return bool
     */
    protected function isAttributeAuditable(Model $model, string $attribute)
    {
        return true;
    }
}
