<?php

namespace Modules\ActivityLog\Entities\Services;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Modules\ActivityLog\Entities\EnhancedActivityLog;

class ActivityLoggerService
{
    /** UUID shared by all writes within the same HTTP request lifecycle. */
    private static ?string $currentRequestId = null;

    /** True for the first write in a request; false for every subsequent one. */
    private static bool $isPrimaryNext = true;

    private function getRequestId(): string
    {
        if (self::$currentRequestId === null) {
            self::$currentRequestId = (string) Str::uuid();
        }
        return self::$currentRequestId;
    }

    private function consumePrimary(): int
    {
        $was = self::$isPrimaryNext;
        self::$isPrimaryNext = false;
        return $was ? 1 : 0;
    }

    protected array $sensitiveFields = [
        'password',
        'remember_token',
        'api_token',
        'two_factor_secret',
        'two_factor_recovery_codes',
        'woocommerce_api_settings',
    ];

    protected array $excludedFields = [
        'created_at',
        'updated_at',
        'deleted_at',
    ];

    /** Fields that add no useful audit information on expense transactions. */
    protected array $expenseExcludedFields = [
        'is_kitchen_order',
        'is_quotation',
        'rp_redeemed',
        'rp_redeemed_amount',
        'shipping_charges',
        'is_export',
        'is_direct_sale',
        'is_suspend',
        'essentials_duration',
        'essentials_amount_per_unit_duration',
        'crm_is_order_request',
        'rp_earned',
        'is_recurring',
    ];

    public function logCreated(Model $model): void
    {
        try {
            $afterData = $this->filterData($model->getAttributes(), $model);

            $this->write([
                'action'         => 'create',
                'model'          => $model,
                'before_data'    => null,
                'after_data'     => $afterData,
                'changed_fields' => array_keys($afterData),
            ]);
        } catch (\Throwable $e) {
            Log::error('EnhancedActivityLog::logCreated failed', [
                'model' => get_class($model),
                'id'    => $model->getKey(),
                'error' => $e->getMessage(),
            ]);
        }
    }

    public function logUpdated(Model $model, array $originalData, array $dirtyFields): void
    {
        try {
            $filteredDirty = $this->filterData($dirtyFields, $model);

            if (empty($filteredDirty)) {
                return;
            }

            $changedFieldNames = array_keys($filteredDirty);

            $beforeData = $this->filterData(
                array_intersect_key($originalData, array_flip($changedFieldNames)),
                $model
            );

            $this->write([
                'action'         => 'update',
                'model'          => $model,
                'before_data'    => $beforeData,
                'after_data'     => $filteredDirty,
                'changed_fields' => $changedFieldNames,
            ]);
        } catch (\Throwable $e) {
            Log::error('EnhancedActivityLog::logUpdated failed', [
                'model' => get_class($model),
                'id'    => $model->getKey(),
                'error' => $e->getMessage(),
            ]);
        }
    }

    public function logAuth(string $action, $user): void
    {
        try {
            $name = trim(
                ($user->surname    ? $user->surname . ' '    : '') .
                ($user->first_name ? $user->first_name . ' ' : '') .
                ($user->last_name  ? $user->last_name        : '')
            ) ?: ('User #' . ($user->id ?? '?'));

            // During login the Timezone middleware hasn't run yet (user is not
            // authenticated), so PHP's default timezone is still APP_TIMEZONE
            // (Asia/Kolkata) rather than the business timezone.
            // Resolve the business timezone explicitly so created_at is correct.
            $timezone = config('app.timezone');
            try {
                $tz = $user->business->time_zone ?? null;
                if (!empty($tz)) {
                    $timezone = $tz;
                }
            } catch (\Throwable) { /* business relation unavailable */ }

            EnhancedActivityLog::create([
                'user_id'        => $user->id ?? null,
                'business_id'    => $user->business_id ?? null,
                'action'         => $action,
                'model_type'     => 'auth',
                'model_id'       => $user->id ?? null,
                'model_label'    => $name,
                'before_data'    => null,
                'after_data'     => null,
                'changed_fields' => [],
                'request_url'    => $this->resolveRequestUrl(),
                'request_method' => request()->method() ?? null,
                'ip_address'     => request()->ip() ?? null,
                'user_agent'     => Str::limit(request()->userAgent() ?? '', 490),
                'created_at'     => \Carbon\Carbon::now($timezone),
            ]);
        } catch (\Throwable $e) {
            Log::error('EnhancedActivityLog::logAuth failed: ' . $e->getMessage());
        }
    }

    public function logDeleted(Model $model, array $snapshotData): void
    {
        try {
            $beforeData = $this->filterData($snapshotData, $model);

            $this->write([
                'action'         => 'delete',
                'model'          => $model,
                'before_data'    => $beforeData,
                'after_data'     => null,
                'changed_fields' => array_keys($beforeData),
            ]);
        } catch (\Throwable $e) {
            Log::error('EnhancedActivityLog::logDeleted failed', [
                'model' => get_class($model),
                'id'    => $model->getKey(),
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Log a custom, named business action against a subject model with explicit
     * before/after snapshots. Used for actions that are not plain model CRUD
     * (e.g. landed-cost apply / rollback). Safe no-op on failure.
     *
     * @param  string      $action   e.g. 'landed_cost_applied'
     * @param  array|null  $before   arbitrary before-state map (already audit-safe)
     * @param  array|null  $after    arbitrary after-state map
     * @param  array|null  $changedFields  defaults to the union of before/after keys
     */
    public function logCustom(Model $subject, string $action, ?array $before, ?array $after, ?array $changedFields = null): void
    {
        try {
            if ($changedFields === null) {
                $changedFields = array_values(array_unique(array_merge(
                    array_keys($before ?? []),
                    array_keys($after ?? [])
                )));
            }

            $this->write([
                'action'         => $action,
                'model'          => $subject,
                'before_data'    => $before,
                'after_data'     => $after,
                'changed_fields' => $changedFields,
            ]);
        } catch (\Throwable $e) {
            Log::error('EnhancedActivityLog::logCustom failed', [
                'model'  => get_class($subject),
                'id'     => $subject->getKey(),
                'action' => $action,
                'error'  => $e->getMessage(),
            ]);
        }
    }

    protected function write(array $payload): void
    {
        $model = $payload['model'];

        EnhancedActivityLog::create([
            'user_id'        => $this->resolveUserId(),
            'business_id'    => $this->resolveBusinessId($model),
            'action'         => $payload['action'],
            'model_type'     => $this->resolveModelType($model),
            'model_id'       => $model->getKey(),
            'model_label'    => $this->resolveModelLabel($model),
            'before_data'    => $payload['before_data'],
            'after_data'     => $payload['after_data'],
            'changed_fields' => $payload['changed_fields'],
            'request_url'    => $this->resolveRequestUrl(),
            'request_method' => request()->method() ?? null,
            'ip_address'     => request()->ip() ?? null,
            'user_agent'     => Str::limit(request()->userAgent() ?? '', 490),
            'request_id'     => $this->getRequestId(),
            'is_primary'     => $this->consumePrimary(),
        ]);
    }

    protected function filterData(array $data, ?Model $model = null): array
    {
        $skip = array_merge($this->sensitiveFields, $this->excludedFields);

        if ($model instanceof \App\Transaction) {
            $type = $model->getAttributes()['type'] ?? null;
            if (in_array($type, ['expense', 'expense_refund'], true)) {
                $skip = array_merge($skip, $this->expenseExcludedFields);
            }
        }

        return array_filter(
            $data,
            fn ($key) => !in_array($key, $skip, true),
            ARRAY_FILTER_USE_KEY
        );
    }

    protected function resolveUserId(): ?int
    {
        try {
            return auth()->check() ? auth()->id() : null;
        } catch (\Throwable) {
            return null;
        }
    }

    protected function resolveBusinessId(Model $model): ?int
    {
        try {
            if (session()->has('user.business_id')) {
                return (int) session('user.business_id');
            }
        } catch (\Throwable) {
            // Session not available (console/queue context)
        }

        if (isset($model->business_id)) {
            return (int) $model->business_id;
        }

        return null;
    }

    /**
     * Returns a virtual model type string.
     * Certain transaction sub-types get their own type so they are
     * distinguishable from regular purchase/sell transactions in the log.
     */
    protected function resolveModelType(Model $model): string
    {
        if ($model instanceof \App\Transaction) {
            $type = $model->getAttributes()['type'] ?? null;
            if ($type === 'opening_stock') {
                return 'opening_stock';
            }
            if (in_array($type, ['expense', 'expense_refund'], true)) {
                return 'expense';
            }
        }

        return get_class($model);
    }

    protected function resolveModelLabel(Model $model): string
    {
        $attrs = $model->getAttributes();

        // Opening stock: show the product name instead of a meaningless transaction ID.
        if ($model instanceof \App\Transaction
            && ($attrs['type'] ?? null) === 'opening_stock'
            && !empty($attrs['opening_stock_product_id'])
        ) {
            try {
                $product = \App\Product::select('id', 'name', 'sku')
                    ->find((int) $attrs['opening_stock_product_id']);
                if ($product) {
                    return $product->name . (!empty($product->sku) ? ' (' . $product->sku . ')' : '');
                }
            } catch (\Throwable) { /* fall through */ }
        }

        if (!empty($attrs['invoice_no'])) {
            return (string) $attrs['invoice_no'];
        }
        if (!empty($attrs['ref_no'])) {
            return (string) $attrs['ref_no'];
        }
        if (!empty($attrs['name'])) {
            $label = (string) $attrs['name'];
            if ($model instanceof \App\Product && !empty($attrs['sku'])) {
                $label .= ' (' . $attrs['sku'] . ')';
            }
            return $label;
        }
        if (!empty($attrs['first_name'])) {
            $parts = array_filter([
                $attrs['first_name'] ?? '',
                $attrs['last_name']  ?? '',
            ]);
            return implode(' ', $parts);
        }
        if (!empty($attrs['title'])) {
            return (string) $attrs['title'];
        }

        return '#' . ($model->getKey() ?? 'unknown');
    }

    protected function resolveRequestUrl(): ?string
    {
        try {
            return Str::limit(request()->fullUrl(), 490);
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * Log each line item of a deleted purchase or sell transaction.
     * Called after logDeleted() so the main transaction entry is already primary.
     *
     * @param  Model   $transaction   The deleted Transaction model instance.
     * @param  array[] $lines         Output of purchase_lines/sell_lines toArray().
     * @param  string  $lineModelClass  e.g. \App\PurchaseLine::class
     */
    public function logDeletedLines(Model $transaction, array $lines, string $lineModelClass): void
    {
        try {
            foreach ($lines as $line) {
                // Strip nested relation arrays (product, variation sub-arrays)
                // so only scalar column values end up in before_data.
                $lineAttrs = array_filter($line, fn ($v) => !is_array($v));
                $beforeData = $this->filterData($lineAttrs);

                EnhancedActivityLog::create([
                    'user_id'        => $this->resolveUserId(),
                    'business_id'    => $this->resolveBusinessId($transaction),
                    'action'         => 'delete',
                    'model_type'     => $lineModelClass,
                    'model_id'       => $line['id'] ?? null,
                    'model_label'    => $this->resolveLineLabel($line, $transaction),
                    'before_data'    => $beforeData,
                    'after_data'     => null,
                    'changed_fields' => array_keys($beforeData),
                    'request_url'    => $this->resolveRequestUrl(),
                    'request_method' => request()->method() ?? null,
                    'ip_address'     => request()->ip() ?? null,
                    'user_agent'     => Str::limit(request()->userAgent() ?? '', 490),
                    'request_id'     => $this->getRequestId(),
                    'is_primary'     => 0, // always secondary to the parent transaction entry
                ]);
            }
        } catch (\Throwable $e) {
            Log::error('EnhancedActivityLog::logDeletedLines failed', [
                'transaction_id' => $transaction->getKey(),
                'line_class'     => $lineModelClass,
                'error'          => $e->getMessage(),
            ]);
        }
    }

    private function resolveLineLabel(array $line, Model $transaction): string
    {
        $txAttrs  = $transaction->getAttributes();
        $txLabel  = $txAttrs['invoice_no'] ?? $txAttrs['ref_no'] ?? ('#' . $transaction->getKey());

        // Product name may come from the eager-loaded 'product' relation sub-array
        $productName   = $line['product']['name']    ?? null;
        $variationName = $line['variation']['name']  ?? null;

        if ($productName) {
            $label = $productName;
            if ($variationName && strtoupper($variationName) !== 'DUMMY') {
                $label .= ' — ' . $variationName;
            }
            return $txLabel . ' › ' . $label;
        }

        return $txLabel . ' › Line #' . ($line['id'] ?? '?');
    }
}
