<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Group;
use App\Models\GroupAsset;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\StreamedResponse;

class GroupAssetService
{
    private const STORAGE_DISK = 'local';

    public function __construct(
        private readonly GroupHistoryService $history,
        private readonly GroupActivityService $activity,
    ) {
    }

    public function store(Group $group, User $actor, array $attributes): GroupAsset
    {
        $file = $attributes['file'];
        if (! $file instanceof UploadedFile) {
            throw ValidationException::withMessages([
                'file' => 'A file upload is required for group assets.',
            ]);
        }

        $extension = strtolower((string) ($file->getClientOriginalExtension() ?: $file->extension() ?: 'bin'));
        $filename = (string) Str::uuid() . '.' . $extension;
        $directory = 'group-assets/' . (int) $group->id;
        $storedPath = $file->storeAs($directory, $filename, self::STORAGE_DISK);
        $mime = strtolower((string) ($file->getMimeType() ?: 'application/octet-stream'));

        $asset = GroupAsset::query()->create([
            'group_id' => (int) $group->id,
            'title' => trim((string) $attributes['title']),
            'description' => $this->nullableString($attributes['description'] ?? null),
            'category' => (string) ($attributes['category'] ?? GroupAsset::CATEGORY_MISC),
            'file_path' => (string) $storedPath,
            'preview_path' => null,
            'visibility' => (string) ($attributes['visibility'] ?? GroupAsset::VISIBILITY_MEMBERS_ONLY),
            'status' => (string) ($attributes['status'] ?? GroupAsset::STATUS_ACTIVE),
            'linked_project_id' => $this->normalizeProjectId($group, $attributes['linked_project_id'] ?? null),
            'uploaded_by_user_id' => (int) $actor->id,
            'approved_by_user_id' => $group->canManageAssets($actor) ? (int) $actor->id : null,
            'is_featured' => (bool) ($attributes['is_featured'] ?? false),
            'file_meta_json' => [
                'original_name' => $file->getClientOriginalName(),
                'mime_type' => $mime,
                'size' => (int) $file->getSize(),
                'extension' => $extension,
            ],
        ]);

        $this->history->record(
            $group,
            $actor,
            'asset_uploaded',
            sprintf('Uploaded asset "%s".', $asset->title),
            'group_asset',
            (int) $asset->id,
            null,
            $asset->only(['title', 'category', 'visibility', 'status'])
        );

        $this->activity->record(
            $group,
            $actor,
            'asset_uploaded',
            'group_asset',
            (int) $asset->id,
            sprintf('%s uploaded a new group asset: %s', $actor->name ?: $actor->username ?: 'A member', $asset->title),
            $asset->description,
            $asset->visibility === GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD ? 'public' : 'internal',
        );

        return $asset->fresh(['group', 'uploader.profile', 'approver.profile', 'linkedProject']);
    }

    public function update(GroupAsset $asset, User $actor, array $attributes): GroupAsset
    {
        $before = $asset->only(['title', 'description', 'category', 'visibility', 'status', 'linked_project_id', 'is_featured']);
        $wasActive = $asset->status === GroupAsset::STATUS_ACTIVE;

        $asset->fill([
            'title' => trim((string) ($attributes['title'] ?? $asset->title)),
            'description' => array_key_exists('description', $attributes) ? $this->nullableString($attributes['description']) : $asset->description,
            'category' => (string) ($attributes['category'] ?? $asset->category),
            'visibility' => (string) ($attributes['visibility'] ?? $asset->visibility),
            'status' => (string) ($attributes['status'] ?? $asset->status),
            'linked_project_id' => array_key_exists('linked_project_id', $attributes) ? $this->normalizeProjectId($asset->group, $attributes['linked_project_id']) : $asset->linked_project_id,
            'is_featured' => (bool) ($attributes['is_featured'] ?? $asset->is_featured),
            'approved_by_user_id' => (string) ($attributes['status'] ?? $asset->status) === GroupAsset::STATUS_ACTIVE ? (int) $actor->id : $asset->approved_by_user_id,
        ])->save();

        if (! $wasActive && $asset->status === GroupAsset::STATUS_ACTIVE && $asset->uploader && (int) $asset->uploader->id !== (int) $actor->id) {
            app(NotificationService::class)->notifyGroupAssetApproved($asset->uploader, $actor, $asset->group, $asset);
        }

        $this->history->record(
            $asset->group,
            $actor,
            'asset_updated',
            sprintf('Updated asset "%s".', $asset->title),
            'group_asset',
            (int) $asset->id,
            $before,
            $asset->only(['title', 'description', 'category', 'visibility', 'status', 'linked_project_id', 'is_featured'])
        );

        return $asset->fresh(['group', 'uploader.profile', 'approver.profile', 'linkedProject']);
    }

    public function studioListing(Group $group, User $viewer, array $filters = []): array
    {
        $bucket = (string) ($filters['bucket'] ?? 'all');
        $category = (string) ($filters['category'] ?? 'all');
        $search = trim((string) ($filters['q'] ?? ''));
        $page = max(1, (int) ($filters['page'] ?? 1));
        $perPage = min(max((int) ($filters['per_page'] ?? 20), 10), 50);

        $query = GroupAsset::query()
            ->with(['uploader.profile', 'approver.profile', 'linkedProject'])
            ->where('group_id', $group->id);

        if ($bucket !== 'all') {
            $query->where('visibility', $bucket);
        }

        if ($category !== 'all') {
            $query->where('category', $category);
        }

        if ($search !== '') {
            $query->where(function ($builder) use ($search): void {
                $builder->where('title', 'like', '%' . $search . '%')
                    ->orWhere('description', 'like', '%' . $search . '%')
                    ->orWhere('file_meta_json->original_name', 'like', '%' . $search . '%');
            });
        }

        if (! $group->canViewInternalAssets($viewer)) {
            $query->whereIn('visibility', [GroupAsset::VISIBILITY_MEMBERS_ONLY, GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD]);
        }

        $paginator = $query->latest('updated_at')->paginate($perPage, ['*'], 'page', $page);

        return [
            'items' => collect($paginator->items())->map(fn (GroupAsset $asset): array => $this->mapStudioAsset($asset))->values()->all(),
            'meta' => [
                'current_page' => $paginator->currentPage(),
                'last_page' => $paginator->lastPage(),
                'per_page' => $paginator->perPage(),
                'total' => $paginator->total(),
            ],
            'filters' => [
                'bucket' => $bucket,
                'category' => $category,
                'q' => $search,
            ],
            'bucket_options' => [
                ['value' => 'all', 'label' => 'All'],
                ['value' => GroupAsset::VISIBILITY_INTERNAL, 'label' => 'Internal'],
                ['value' => GroupAsset::VISIBILITY_MEMBERS_ONLY, 'label' => 'Members only'],
                ['value' => GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD, 'label' => 'Public download'],
            ],
        ];
    }

    public function publicListing(Group $group, int $limit = 12): array
    {
        return GroupAsset::query()
            ->with(['uploader.profile', 'linkedProject'])
            ->where('group_id', $group->id)
            ->where('status', GroupAsset::STATUS_ACTIVE)
            ->where('visibility', GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD)
            ->latest('updated_at')
            ->limit($limit)
            ->get()
            ->map(fn (GroupAsset $asset): array => $this->mapPublicAsset($asset))
            ->values()
            ->all();
    }

    public function downloadResponse(GroupAsset $asset): StreamedResponse
    {
        $name = (string) ($asset->file_meta_json['original_name'] ?? basename((string) $asset->file_path));
        $mime = (string) ($asset->file_meta_json['mime_type'] ?? 'application/octet-stream');

        return Storage::disk(self::STORAGE_DISK)->download((string) $asset->file_path, $name, [
            'Content-Type' => $mime,
        ]);
    }

    public function mapStudioAsset(GroupAsset $asset): array
    {
        return [
            'id' => (int) $asset->id,
            'title' => (string) $asset->title,
            'description' => $asset->description,
            'category' => (string) $asset->category,
            'visibility' => (string) $asset->visibility,
            'status' => (string) $asset->status,
            'is_featured' => (bool) $asset->is_featured,
            'file_meta' => $asset->file_meta_json ?? [],
            'linked_project' => $asset->linkedProject ? ['id' => (int) $asset->linkedProject->id, 'title' => $asset->linkedProject->title] : null,
            'download_url' => route('groups.assets.download', ['group' => $asset->group, 'asset' => $asset]),
            'urls' => [
                'edit' => route('studio.groups.assets.update', ['group' => $asset->group, 'asset' => $asset]),
            ],
        ];
    }

    public function mapPublicAsset(GroupAsset $asset): array
    {
        return [
            'id' => (int) $asset->id,
            'title' => (string) $asset->title,
            'description' => $asset->description,
            'category' => (string) $asset->category,
            'download_url' => route('groups.assets.download', ['group' => $asset->group, 'asset' => $asset]),
        ];
    }

    private function normalizeProjectId(Group $group, mixed $projectId): ?int
    {
        $id = (int) $projectId;
        return $id > 0 && $group->projects()->where('id', $id)->exists() ? $id : null;
    }

    private function nullableString(mixed $value): ?string
    {
        $trimmed = trim((string) $value);
        return $trimmed !== '' ? $trimmed : null;
    }
}