<?php

declare(strict_types=1);

namespace App\Service\Import\Importers;

use App\Enums\Role;
use App\Service\TimezoneService;
use Exception;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Override;
use Spatie\TemporaryDirectory\TemporaryDirectory;
use ValueError;
use ZipArchive;

class TogglDataImporter extends DefaultImporter
{
    /**
     * @throws ImportException
     */
    #[Override]
    public function importData(string $data, string $timezone): void
    {
        $temporaryDirectoryZip = null;
        $temporaryDirectory = null;
        try {
            $zip = new ZipArchive;
            $temporaryDirectoryZip = TemporaryDirectory::make();
            file_put_contents($temporaryDirectoryZip->path('import.zip'), $data);
            $res = $zip->open($temporaryDirectoryZip->path('import.zip'), ZipArchive::RDONLY);
            if ($res !== true) {
                throw new ImportException('Invalid ZIP, error code: '.$res);
            }
            $temporaryDirectory = TemporaryDirectory::make();
            $zip->extractTo($temporaryDirectory->path());
            $zip->close();
            if (! file_exists($temporaryDirectory->path('clients.json'))) {
                throw new ImportException('File "clients.json" missing in ZIP');
            }
            $clientsFileContent = file_get_contents($temporaryDirectory->path('clients.json'));
            if ($clientsFileContent === false) {
                throw new ImportException('File "clients.json" can not be opened');
            }
            $clients = json_decode($clientsFileContent);
            if ($clients === null) {
                throw new ImportException('File "clients.json" is empty');
            }
            if (! file_exists($temporaryDirectory->path('projects.json'))) {
                throw new ImportException('File "projects.json" missing in ZIP');
            }
            $projectsFileContent = file_get_contents($temporaryDirectory->path('projects.json'));
            if ($projectsFileContent === false) {
                throw new ImportException('File "projects.json" can not be opened');
            }
            $projects = json_decode($projectsFileContent);
            if ($projects === null) {
                throw new ImportException('File "projects.json" is empty');
            }
            if (! file_exists($temporaryDirectory->path('tags.json'))) {
                throw new ImportException('File "tags.json" missing in ZIP');
            }
            $tagsFileContent = file_get_contents($temporaryDirectory->path('tags.json'));
            if ($tagsFileContent === false) {
                throw new ImportException('File "tags.json" can not be opened');
            }
            $tags = json_decode($tagsFileContent);
            if ($tags === null) {
                throw new ImportException('File "tags.json" is empty');
            }
            if (! file_exists($temporaryDirectory->path('workspace_users.json'))) {
                throw new ImportException('File "workspace_users.json" missing in ZIP');
            }
            $workspaceUsersFileContent = file_get_contents($temporaryDirectory->path('workspace_users.json'));
            if ($workspaceUsersFileContent === false) {
                throw new ImportException('File "workspace_users.json" can not be opened');
            }
            $workspaceUsers = json_decode($workspaceUsersFileContent);
            if ($workspaceUsers === null) {
                throw new ImportException('File "workspace_users.json" is empty');
            }
            foreach ($clients as $client) {
                $this->clientImportHelper->getKey([
                    'name' => $client->name,
                    'organization_id' => $this->organization->id,
                ], [
                    'archived_at' => $client->archived === true ? Carbon::now() : null,
                ], (string) $client->id);
            }
            foreach ($tags as $tag) {
                $this->tagImportHelper->getKey([
                    'name' => $tag->name,
                    'organization_id' => $this->organization->id,
                ], [], (string) $tag->id);
            }

            foreach ($workspaceUsers as $workspaceUser) {
                $timezone = Str::trim($workspaceUser->timezone);
                if ($timezone === '') {
                    $timezone = 'UTC';
                }
                if (! app(TimezoneService::class)->isValid($timezone)) {
                    Log::warning('TogglDateImporter: Invalid timezone', [
                        'timezone' => $timezone,
                    ]);
                    $timezone = 'UTC';
                }

                $userId = $this->userImportHelper->getKey([
                    'email' => $workspaceUser->email,
                ], [
                    'name' => $workspaceUser->name,
                    'timezone' => $timezone,
                    'is_placeholder' => true,
                ], (string) $workspaceUser->uid);
                $this->memberImportHelper->getKey([
                    'user_id' => $userId,
                    'organization_id' => $this->organization->getKey(),
                ], [
                    'role' => Role::Placeholder->value,
                ], $userId);
            }

            foreach ($projects as $project) {
                $projectExternalId = $this->guardExternalIdentifier($project->id);
                $clientId = null;
                if ($project->client_id !== null) {
                    $clientId = $this->clientImportHelper->getKeyByExternalIdentifier((string) $project->client_id);
                    if ($clientId === null) {
                        throw new Exception('Client does not exist');
                    }
                }

                if (! $this->colorService->isValid($project->color)) {
                    throw new ImportException('Invalid color');
                }

                $projectId = $this->projectImportHelper->getKey([
                    'name' => $project->name,
                    'client_id' => $clientId,
                    'organization_id' => $this->organization->getKey(),
                ], [
                    'color' => $project->color,
                    'is_billable' => $project->billable,
                    'is_public' => ! $project->is_private,
                    'billable_rate' => $project->rate !== null ? (int) ($project->rate * 100) : null,
                ], (string) $project->id);

                if (! file_exists($temporaryDirectory->path('projects_users/'.$projectExternalId.'.json'))) {
                    throw new ImportException('File "projects_users/'.$projectExternalId.'.json" missing in ZIP');
                }
                $projectMembersFileContent = file_get_contents($temporaryDirectory->path('projects_users/'.$projectExternalId.'.json'));
                if ($projectMembersFileContent === false) {
                    throw new ImportException('File "projects_users/'.$projectExternalId.'.json" can not be opened');
                }
                $projectMembers = json_decode($projectMembersFileContent);
                if ($projectMembers === null) {
                    throw new ImportException('File "projects_users/'.$projectExternalId.'.json" is empty');
                }
                foreach ($projectMembers as $projectMember) {
                    $userId = $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id);
                    $this->projectMemberImportHelper->getKey([
                        'project_id' => $projectId,
                        'member_id' => $this->memberImportHelper->getKeyByExternalIdentifier($userId),
                    ], [
                        'user_id' => $userId,
                        'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null,
                    ]);
                }
            }
            $projectIds = $this->projectImportHelper->getExternalIds();
            foreach ($projectIds as $projectIdExternal) {
                $projectIdExternal = $this->guardExternalIdentifier($projectIdExternal);
                if (! file_exists($temporaryDirectory->path('tasks/'.$projectIdExternal.'.json'))) {
                    continue;
                }
                $tasksFileContent = file_get_contents($temporaryDirectory->path('tasks/'.$projectIdExternal.'.json'));
                if ($tasksFileContent === false) {
                    throw new ImportException('File "tasks/'.$projectIdExternal.'.json" can not be opened');
                }
                $tasks = json_decode($tasksFileContent);
                if ($tasks === null) {
                    throw new ImportException('File "tasks/'.$projectIdExternal.'.json" is empty');
                }
                foreach ($tasks as $task) {
                    $projectId = $this->projectImportHelper->getKeyByExternalIdentifier((string) $projectIdExternal);

                    if ($projectId === null) {
                        throw new Exception('Project does not exist');
                    }
                    $this->taskImportHelper->getKey([
                        'name' => $task->name,
                        'project_id' => $projectId,
                        'organization_id' => $this->organization->getKey(),
                    ], [
                        'done_at' => $task->active === false ? Carbon::now() : null,
                    ], (string) $task->id);
                }
            }
        } catch (ValueError $exception) {

        } catch (ImportException $exception) {
            throw $exception;
        } catch (Exception $exception) {
            report($exception);
            throw new ImportException('Unknown error');
        } finally {
            $temporaryDirectory?->delete();
            $temporaryDirectoryZip?->delete();
        }
    }

    /**
     * Ensure an externally-sourced identifier can be safely used inside a
     * filesystem path. The identifiers originate from the untrusted uploaded
     * ZIP, and Spatie's TemporaryDirectory::path() auto-creates any missing
     * parent directory of the resolved path, so an unfiltered "../" sequence
     * would escape the import sandbox and create/probe arbitrary paths on the
     * host (CWE-22). Toggl identifiers are numeric, so restricting them to a
     * conservative allow-list rejects traversal without affecting real data.
     *
     * @throws ImportException
     */
    private function guardExternalIdentifier(mixed $id): string
    {
        if (! is_string($id) && ! is_int($id)) {
            throw new ImportException('Invalid identifier in import data');
        }
        $id = (string) $id;
        if (preg_match('/^[A-Za-z0-9_-]+$/', $id) !== 1) {
            throw new ImportException('Invalid identifier in import data');
        }

        return $id;
    }

    #[Override]
    public function getName(): string
    {
        return __('importer.toggl_data_importer.name');
    }

    #[Override]
    public function getDescription(): string
    {
        return __('importer.toggl_data_importer.description');
    }
}
