<?php

declare(strict_types=1);

namespace Maestro\Core\Services;

use Illuminate\Support\Facades\File;
use Maestro\Core\Support\HostStubManifest;
use Symfony\Component\Process\Process;

final class MaestroLinkService
{
    public function __construct(
        private readonly HostIntegrityService $integrity,
    ) {}

    public function isLinked(): bool
    {
        return $this->integrity->checkMaestroLinked()['status'] !== 'ok';
    }

    public function linkStatePath(): string
    {
        return base_path(HostStubManifest::LINK_STATE_FILE);
    }

    /**
     * @return array{path: string, linked_at: string, previous_composer_constraint: ?string}
     */
    public function readLinkState(): ?array
    {
        $path = $this->linkStatePath();
        if (! File::exists($path)) {
            return null;
        }

        $json = json_decode(File::get($path), true);
        if (! is_array($json) || ! isset($json['path']) || ! is_string($json['path'])) {
            return null;
        }

        return [
            'path' => $json['path'],
            'linked_at' => is_string($json['linked_at'] ?? null) ? $json['linked_at'] : '',
            'previous_composer_constraint' => is_string($json['previous_composer_constraint'] ?? null)
                ? $json['previous_composer_constraint']
                : null,
        ];
    }

    public function validateMaestroPackagePath(string $path): string
    {
        $resolved = realpath($path);
        if ($resolved === false || ! is_dir($resolved)) {
            throw new \InvalidArgumentException('Maestro CORE path does not exist: '.$path);
        }

        $composerJson = $resolved.DIRECTORY_SEPARATOR.'composer.json';
        $packageJson = $resolved.DIRECTORY_SEPARATOR.'package.json';

        if (! File::exists($composerJson) || ! File::exists($packageJson)) {
            throw new \InvalidArgumentException('Path is not a Maestro CORE repository (missing composer.json or package.json).');
        }

        $composer = json_decode(File::get($composerJson), true);
        $npm = json_decode(File::get($packageJson), true);

        if (! is_array($composer) || ($composer['name'] ?? '') !== 'maestro/core') {
            throw new \InvalidArgumentException('composer.json name must be maestro/core.');
        }

        if (! is_array($npm) || ($npm['name'] ?? '') !== '@maestro/core') {
            throw new \InvalidArgumentException('package.json name must be @maestro/core.');
        }

        return $resolved;
    }

    /**
     * @param  callable(string): void|null  $output
     */
    public function link(string $maestroPath, ?callable $output = null): void
    {
        $resolved = $this->validateMaestroPackagePath($maestroPath);

        if ($this->isLinked()) {
            throw new \RuntimeException('Maestro is already linked. Run php artisan maestro:unlink first.');
        }

        $previousConstraint = $this->readComposerConstraint();

        $this->mergeComposerPathRepository($resolved);
        $this->runProcess(['composer', 'require', 'maestro/core:@dev', '--no-interaction'], base_path(), $output);

        $this->runProcess(
            ['npm', 'install', 'file:'.$resolved, '--no-fund', '--no-audit'],
            base_path(),
            $output,
        );

        File::put($this->linkStatePath(), json_encode([
            'path' => $resolved,
            'linked_at' => now()->toIso8601String(),
            'previous_composer_constraint' => $previousConstraint,
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL);

        $this->appendEnvLinkedHint();
    }

    /**
     * @param  callable(string): void|null  $output
     */
    public function unlink(?callable $output = null): void
    {
        $state = $this->readLinkState();
        $constraint = $state['previous_composer_constraint'] ?? '^1.0';

        $this->removeComposerPathRepository();
        $this->runProcess(
            ['composer', 'require', 'maestro/core:'.$constraint, '--no-interaction'],
            base_path(),
            $output,
        );

        if (File::exists(base_path('package-lock.json'))) {
            $this->runProcess(['npm', 'ci', '--no-fund', '--no-audit'], base_path(), $output);
        } else {
            $this->runProcess(['npm', 'install', '--no-fund', '--no-audit'], base_path(), $output);
        }

        if (File::exists($this->linkStatePath())) {
            File::delete($this->linkStatePath());
        }
    }

    private function readComposerConstraint(): ?string
    {
        $composerPath = base_path('composer.json');
        if (! File::exists($composerPath)) {
            return null;
        }

        $data = json_decode(File::get($composerPath), true);
        if (! is_array($data)) {
            return null;
        }

        $require = $data['require']['maestro/core'] ?? null;

        return is_string($require) ? $require : null;
    }

    private function mergeComposerPathRepository(string $resolvedPath): void
    {
        $composerPath = base_path('composer.json');
        if (! File::exists($composerPath)) {
            throw new \RuntimeException('composer.json not found in project root.');
        }

        $data = json_decode(File::get($composerPath), true);
        if (! is_array($data)) {
            throw new \RuntimeException('composer.json is invalid JSON.');
        }

        $repos = $data['repositories'] ?? [];
        $repos = is_array($repos) ? $repos : [];

        $key = HostStubManifest::COMPOSER_PATH_REPO_KEY;
        $repos[$key] = [
            'type' => 'path',
            'url' => $resolvedPath,
            'options' => ['symlink' => true],
        ];

        $data['repositories'] = $repos;
        File::put($composerPath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL);
    }

    private function removeComposerPathRepository(): void
    {
        $composerPath = base_path('composer.json');
        if (! File::exists($composerPath)) {
            return;
        }

        $data = json_decode(File::get($composerPath), true);
        if (! is_array($data)) {
            return;
        }

        $repos = $data['repositories'] ?? [];
        if (! is_array($repos)) {
            return;
        }

        unset($repos[HostStubManifest::COMPOSER_PATH_REPO_KEY]);
        $data['repositories'] = $repos;
        File::put($composerPath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL);
    }

    private function appendEnvLinkedHint(): void
    {
        $envPath = base_path('.env');
        if (! File::exists($envPath)) {
            return;
        }

        $contents = File::get($envPath);
        if (preg_match('/^MAESTRO_LINKED=/m', $contents)) {
            $contents = preg_replace('/^MAESTRO_LINKED=.*$/m', 'MAESTRO_LINKED=true', $contents) ?? $contents;
        } else {
            $contents = rtrim($contents).PHP_EOL.'MAESTRO_LINKED=true'.PHP_EOL;
        }

        File::put($envPath, $contents);
    }

    /**
     * @param  list<string>  $command
     * @param  callable(string): void|null  $output
     */
    private function runProcess(array $command, string $cwd, ?callable $output): void
    {
        $process = new Process($command, $cwd);
        $process->setTimeout(600);

        if ($output !== null) {
            $process->run(function (string $type, string $buffer) use ($output): void {
                $output($buffer);
            });
        } else {
            $process->run();
        }

        if (! $process->isSuccessful()) {
            throw new \RuntimeException(
                'Command failed: '.implode(' ', $command).PHP_EOL.$process->getErrorOutput().$process->getOutput(),
            );
        }
    }
}
