<?php

declare(strict_types=1);

namespace App\Services\Settings;

use App\Models\PlatformUser;
use App\Models\ManagedEnvironment;
use App\Models\TenantSetting;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceSetting;
use App\Models\WorkspaceSubscription;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\Capabilities;
use App\Support\Auth\PlatformCapabilities;
use App\Support\Settings\SettingDefinition;
use App\Support\Settings\SettingsRegistry;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class SettingsWriter
{
    public function __construct(
        private SettingsRegistry $registry,
        private SettingsResolver $resolver,
        private WorkspaceAuditLogger $auditLogger,
        private WorkspaceCapabilityResolver $workspaceCapabilityResolver,
    ) {}

    public function updateWorkspaceSetting(User $actor, Workspace $workspace, string $domain, string $key, mixed $value): WorkspaceSetting
    {
        $this->authorizeManage($actor, $workspace);

        $result = $this->persistWorkspaceSetting($workspace, $domain, $key, $value, (int) $actor->getKey());

        $this->resolver->clearCache();

        $afterValue = $this->resolver->resolveValue($workspace, $domain, $key);

        $this->auditLogger->log(
            workspace: $workspace,
            action: AuditActionId::WorkspaceSettingUpdated->value,
            context: [
                'metadata' => [
                    'scope' => 'workspace',
                    'domain' => $domain,
                    'key' => $key,
                    'before_value' => $result['before_value'],
                    'after_value' => $afterValue,
                ],
            ],
            actor: $actor,
            resourceType: 'workspace_setting',
            resourceId: $domain.'.'.$key,
        );

        return $result['setting'];
    }

    public function updateWorkspaceCommercialLifecycle(
        PlatformUser $actor,
        Workspace $workspace,
        string $state,
        string $reason,
    ): void {
        $state = strtolower(trim($state));
        $reason = trim($reason);

        if (! $actor->hasCapability(PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE)) {
            throw new AuthorizationException('Missing commercial lifecycle manage capability.');
        }

        if ($reason === '') {
            throw ValidationException::withMessages([
                'reason' => ['A rationale is required when changing commercial lifecycle state.'],
            ]);
        }

        DB::transaction(function () use ($actor, $workspace, $state, $reason): void {
            $stateResult = $this->persistWorkspaceSetting(
                workspace: $workspace,
                domain: WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN,
                key: WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE,
                value: $state,
                updatedByUserId: null,
            );

            $reasonResult = $this->persistWorkspaceSetting(
                workspace: $workspace,
                domain: WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN,
                key: WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_REASON,
                value: $reason,
                updatedByUserId: null,
            );

            $this->resolver->clearCache();

            $afterState = $this->resolver->resolveValue(
                $workspace,
                WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN,
                WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE,
            );

            $afterReason = $this->resolver->resolveValue(
                $workspace,
                WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN,
                WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_REASON,
            );

            $this->auditLogger->log(
                workspace: $workspace,
                action: AuditActionId::WorkspaceSettingUpdated->value,
                context: [
                    'metadata' => [
                        'scope' => 'workspace',
                        'domain' => WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN,
                        'key' => WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE,
                        'before_state' => $stateResult['before_value'],
                        'after_state' => $afterState,
                        'before_reason' => $reasonResult['before_value'],
                        'after_reason' => $afterReason,
                    ],
                ],
                actor: $actor,
                resourceType: 'workspace_setting',
                resourceId: WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN.'.'.WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE,
                targetLabel: 'Commercial lifecycle state',
            );
        });
    }

    /**
     * @param  array<string, mixed>  $attributes
     */
    public function updateWorkspaceSubscription(
        PlatformUser $actor,
        Workspace $workspace,
        array $attributes,
    ): WorkspaceSubscription {
        $this->authorizeCommercialLifecycleManage($actor);

        $validator = Validator::make($attributes, [
            'state' => ['required', 'string', 'in:'.implode(',', WorkspaceSubscription::stateIds())],
            'billing_reference' => ['nullable', 'string', 'max:191'],
            'trial_ends_at' => ['nullable', 'date'],
            'current_period_starts_at' => ['nullable', 'date'],
            'current_period_ends_at' => ['nullable', 'date'],
            'status_reason' => ['required', 'string', 'max:500'],
        ]);

        if ($validator->fails()) {
            throw ValidationException::withMessages($validator->errors()->toArray());
        }

        $validated = $validator->validated();
        $state = (string) $validated['state'];

        if ($state === WorkspaceSubscription::STATE_TRIAL && blank($validated['trial_ends_at'] ?? null)) {
            throw ValidationException::withMessages([
                'trial_ends_at' => ['A trial end date is required for trial subscriptions.'],
            ]);
        }

        if (in_array($state, [
            WorkspaceSubscription::STATE_ACTIVE,
            WorkspaceSubscription::STATE_PAST_DUE,
            WorkspaceSubscription::STATE_CANCEL_AT_PERIOD_END,
        ], true) && blank($validated['current_period_starts_at'] ?? null)) {
            throw ValidationException::withMessages([
                'current_period_starts_at' => ['A current period start date is required for this subscription state.'],
            ]);
        }

        if (in_array($state, [
            WorkspaceSubscription::STATE_ACTIVE,
            WorkspaceSubscription::STATE_PAST_DUE,
            WorkspaceSubscription::STATE_CANCEL_AT_PERIOD_END,
            WorkspaceSubscription::STATE_ENDED,
        ], true) && blank($validated['current_period_ends_at'] ?? null)) {
            throw ValidationException::withMessages([
                'current_period_ends_at' => ['A current period end date is required for this subscription state.'],
            ]);
        }

        return DB::transaction(function () use ($actor, $workspace, $validated): WorkspaceSubscription {
            $workspace->loadMissing('subscription');

            $before = $workspace->subscription instanceof WorkspaceSubscription
                ? $this->workspaceSubscriptionAuditPayload($workspace->subscription)
                : null;

            $subscription = WorkspaceSubscription::query()->updateOrCreate(
                ['workspace_id' => (int) $workspace->getKey()],
                [
                    'state' => (string) $validated['state'],
                    'billing_reference' => filled($validated['billing_reference'] ?? null)
                        ? trim((string) $validated['billing_reference'])
                        : null,
                    'trial_ends_at' => filled($validated['trial_ends_at'] ?? null)
                        ? Carbon::parse((string) $validated['trial_ends_at'])
                        : null,
                    'current_period_starts_at' => filled($validated['current_period_starts_at'] ?? null)
                        ? Carbon::parse((string) $validated['current_period_starts_at'])
                        : null,
                    'current_period_ends_at' => filled($validated['current_period_ends_at'] ?? null)
                        ? Carbon::parse((string) $validated['current_period_ends_at'])
                        : null,
                    'status_reason' => trim((string) $validated['status_reason']),
                ],
            );

            $workspace->setRelation('subscription', $subscription->fresh());

            $this->auditLogger->log(
                workspace: $workspace,
                action: AuditActionId::WorkspaceSubscriptionUpdated,
                context: [
                    'metadata' => [
                        'before' => $before,
                        'after' => $this->workspaceSubscriptionAuditPayload($subscription),
                    ],
                ],
                actor: $actor,
                resourceType: 'workspace_subscription',
                resourceId: (string) $subscription->getKey(),
                targetLabel: 'Current workspace subscription',
                summary: 'Workspace subscription updated',
            );

            return $subscription;
        });
    }

    public function resetWorkspaceSetting(User $actor, Workspace $workspace, string $domain, string $key): void
    {
        $this->authorizeManage($actor, $workspace);

        $this->requireDefinition($domain, $key);

        $existing = WorkspaceSetting::query()
            ->where('workspace_id', (int) $workspace->getKey())
            ->where('domain', $domain)
            ->where('key', $key)
            ->first();

        $beforeValue = $existing instanceof WorkspaceSetting
            ? $this->decodeStoredValue($existing->getAttribute('value'))
            : null;

        if ($existing instanceof WorkspaceSetting) {
            $existing->delete();
        }

        $this->resolver->clearCache();

        $afterValue = $this->resolver->resolveValue($workspace, $domain, $key);

        $this->auditLogger->log(
            workspace: $workspace,
            action: AuditActionId::WorkspaceSettingReset->value,
            context: [
                'metadata' => [
                    'scope' => 'workspace',
                    'domain' => $domain,
                    'key' => $key,
                    'before_value' => $beforeValue,
                    'after_value' => $afterValue,
                ],
            ],
            actor: $actor,
            resourceType: 'workspace_setting',
            resourceId: $domain.'.'.$key,
        );
    }

    public function updateTenantSetting(User $actor, Workspace $workspace, ManagedEnvironment $tenant, string $domain, string $key, mixed $value): TenantSetting
    {
        $this->authorizeManage($actor, $workspace);
        $this->assertTenantBelongsToWorkspace($workspace, $tenant);

        $definition = $this->requireDefinition($domain, $key);
        $normalizedValue = $this->validatedValue($definition, $value);

        $setting = TenantSetting::query()->updateOrCreate([
            'workspace_id' => (int) $workspace->getKey(),
            'managed_environment_id' => (int) $tenant->getKey(),
            'domain' => $domain,
            'key' => $key,
        ], [
            'value' => $normalizedValue,
            'updated_by_user_id' => (int) $actor->getKey(),
        ]);

        $this->resolver->clearCache();

        return $setting;
    }

    public function resetTenantSetting(User $actor, Workspace $workspace, ManagedEnvironment $tenant, string $domain, string $key): void
    {
        $this->authorizeManage($actor, $workspace);
        $this->assertTenantBelongsToWorkspace($workspace, $tenant);

        $this->requireDefinition($domain, $key);

        TenantSetting::query()
            ->where('workspace_id', (int) $workspace->getKey())
            ->where('managed_environment_id', (int) $tenant->getKey())
            ->where('domain', $domain)
            ->where('key', $key)
            ->delete();

        $this->resolver->clearCache();
    }

    private function requireDefinition(string $domain, string $key): SettingDefinition
    {
        $definition = $this->registry->find($domain, $key);

        if ($definition instanceof SettingDefinition) {
            return $definition;
        }

        throw ValidationException::withMessages([
            'key' => [sprintf('Unknown setting key: %s.%s', $domain, $key)],
        ]);
    }

    /**
     * @return array{setting: WorkspaceSetting, before_value: mixed}
     */
    private function persistWorkspaceSetting(Workspace $workspace, string $domain, string $key, mixed $value, ?int $updatedByUserId): array
    {
        $definition = $this->requireDefinition($domain, $key);
        $normalizedValue = $this->validatedValue($definition, $value);

        $existing = WorkspaceSetting::query()
            ->where('workspace_id', (int) $workspace->getKey())
            ->where('domain', $domain)
            ->where('key', $key)
            ->first();

        $beforeValue = $existing instanceof WorkspaceSetting
            ? $this->decodeStoredValue($existing->getAttribute('value'))
            : null;

        $setting = WorkspaceSetting::query()->updateOrCreate([
            'workspace_id' => (int) $workspace->getKey(),
            'domain' => $domain,
            'key' => $key,
        ], [
            'value' => $normalizedValue,
            'updated_by_user_id' => $updatedByUserId,
        ]);

        return [
            'setting' => $setting,
            'before_value' => $beforeValue,
        ];
    }

    private function validatedValue(SettingDefinition $definition, mixed $value): mixed
    {
        $validator = Validator::make(
            data: ['value' => $value],
            rules: ['value' => $definition->rules],
        );

        if ($validator->fails()) {
            throw ValidationException::withMessages($validator->errors()->toArray());
        }

        return $definition->normalize($validator->validated()['value']);
    }

    private function authorizeManage(User $actor, Workspace $workspace): void
    {
        if (! $this->workspaceCapabilityResolver->isMember($actor, $workspace)) {
            throw new NotFoundHttpException('Workspace not found.');
        }

        if (! $this->workspaceCapabilityResolver->can($actor, $workspace, Capabilities::WORKSPACE_SETTINGS_MANAGE)) {
            throw new AuthorizationException('Missing workspace settings manage capability.');
        }
    }

    private function authorizeCommercialLifecycleManage(PlatformUser $actor): void
    {
        if (! $actor->hasCapability(PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE)) {
            throw new AuthorizationException('Missing commercial lifecycle manage capability.');
        }
    }

    private function assertTenantBelongsToWorkspace(Workspace $workspace, ManagedEnvironment $tenant): void
    {
        if ((int) $tenant->workspace_id !== (int) $workspace->getKey()) {
            throw new NotFoundHttpException('ManagedEnvironment is outside the selected workspace scope.');
        }
    }

    private function decodeStoredValue(mixed $value): mixed
    {
        if (! is_string($value)) {
            return $value;
        }

        $decoded = json_decode($value, true);

        return json_last_error() === JSON_ERROR_NONE ? $decoded : $value;
    }

    /**
     * @return array<string, mixed>
     */
    private function workspaceSubscriptionAuditPayload(WorkspaceSubscription $subscription): array
    {
        return [
            'state' => $subscription->state,
            'billing_reference' => $subscription->billing_reference,
            'trial_ends_at' => $subscription->trial_ends_at?->toAtomString(),
            'current_period_starts_at' => $subscription->current_period_starts_at?->toAtomString(),
            'current_period_ends_at' => $subscription->current_period_ends_at?->toAtomString(),
            'status_reason' => $subscription->status_reason,
        ];
    }
}
