<?php

declare(strict_types=1);

/**
 * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OC\Migration;

use OC\DB\Connection;
use OC\DB\MigrationService;
use OC\Migration\Exceptions\AttributeException;
use OCP\App\IAppManager;
use OCP\Migration\Attributes\GenericMigrationAttribute;
use OCP\Migration\Attributes\MigrationAttribute;
use Psr\Log\LoggerInterface;
use ReflectionClass;

/**
 * Helps managing DB Migrations' Metadata
 *
 * @since 30.0.0
 */
class MetadataManager {
	public function __construct(
		private readonly IAppManager $appManager,
		private readonly Connection $connection,
		private readonly LoggerInterface $logger,
	) {
	}

	/**
	 * We get all migrations from an app (or 'core'), and
	 * for each migration files we extract its attributes
	 *
	 * @param string $appId
	 *
	 * @return array<string, MigrationAttribute[]>
	 * @since 30.0.0
	 */
	public function extractMigrationAttributes(string $appId): array {
		$ms = new MigrationService($appId, $this->connection);

		$metadata = [];
		foreach ($ms->getAvailableVersions() as $version) {
			$metadata[$version] = [];
			$class = new ReflectionClass($ms->createInstance($version));
			$attributes = $class->getAttributes();
			foreach ($attributes as $attribute) {
				$item = $attribute->newInstance();
				if ($item instanceof MigrationAttribute) {
					$metadata[$version][] = $item;
				}
			}
		}

		return $metadata;
	}

	/**
	 * convert direct data from release metadata into a list of Migrations' Attribute
	 *
	 * @param array<array-key, array<array-key, array>> $metadata
	 * @param bool $filterKnownMigrations ignore metadata already done in local instance
	 *
	 * @return array{apps: array<array-key, array<string, MigrationAttribute[]>>, core: array<string, MigrationAttribute[]>}
	 * @since 30.0.0
	 */
	public function getMigrationsAttributesFromReleaseMetadata(
		array $metadata,
		bool $filterKnownMigrations = false,
	): array {
		$appsAttributes = [];
		foreach (array_keys($metadata['apps']) as $appId) {
			if ($filterKnownMigrations && !$this->appManager->isInstalled($appId)) {
				continue; // if not interested and app is not installed
			}

			$done = ($filterKnownMigrations) ? $this->getKnownMigrations($appId) : [];
			$appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? [], $done);
		}

		$done = ($filterKnownMigrations) ? $this->getKnownMigrations('core') : [];
		return [
			'core' => $this->parseMigrations($metadata['core'] ?? [], $done),
			'apps' => $appsAttributes
		];
	}

	/**
	 * returns list of installed apps that does not support migrations metadata (yet)
	 *
	 * @param array<array-key, array<array-key, array>> $metadata
	 *
	 * @return string[]
	 * @since 30.0.0
	 */
	public function getUnsupportedApps(array $metadata): array {
		return array_values(array_diff($this->appManager->getInstalledApps(), array_keys($metadata['apps'])));
	}

	/**
	 * convert raw data to a list of MigrationAttribute
	 *
	 * @param array $migrations
	 * @param array $ignoreMigrations
	 *
	 * @return array<string, MigrationAttribute[]>
	 */
	private function parseMigrations(array $migrations, array $ignoreMigrations = []): array {
		$parsed = [];
		foreach (array_keys($migrations) as $entry) {
			if (in_array($entry, $ignoreMigrations)) {
				continue;
			}

			$parsed[$entry] = [];
			foreach ($migrations[$entry] as $item) {
				try {
					$parsed[$entry][] = $this->createAttribute($item);
				} catch (AttributeException $e) {
					$this->logger->warning('exception while trying to create attribute', ['exception' => $e, 'item' => json_encode($item)]);
					$parsed[$entry][] = new GenericMigrationAttribute($item);
				}
			}
		}

		return $parsed;
	}

	/**
	 * returns migrations already done
	 *
	 * @param string $appId
	 *
	 * @return array
	 * @throws \Exception
	 */
	private function getKnownMigrations(string $appId): array {
		$ms = new MigrationService($appId, $this->connection);
		return $ms->getMigratedVersions();
	}

	/**
	 * generate (deserialize) a MigrationAttribute from a serialized version
	 *
	 * @param array $item
	 *
	 * @return MigrationAttribute
	 * @throws AttributeException
	 */
	private function createAttribute(array $item): MigrationAttribute {
		$class = $item['class'] ?? '';
		$namespace = 'OCP\Migration\Attributes\\';
		if (!str_starts_with($class, $namespace)
			|| !ctype_alpha(substr($class, strlen($namespace)))) {
			throw new AttributeException('class name does not looks valid');
		}

		try {
			$attribute = new $class($item['table'] ?? '');
			return $attribute->import($item);
		} catch (\Error) {
			throw new AttributeException('cannot import Attribute');
		}
	}
}