<?php

declare(strict_types=1);

namespace StaticPHP\Config;

use StaticPHP\Exception\ValidationException;

class ConfigValidator
{
    /**
     * Global field type definitions
     * Maps field names to their expected types and validation rules
     * Note: This only includes fields used in config files (source.json, lib.json, ext.json, pkg.json, pre-built.json)
     */
    public const array PACKAGE_FIELD_TYPES = [
        // package fields
        'type' => ConfigType::STRING,
        'description' => ConfigType::STRING,
        'depends' => ConfigType::LIST_ARRAY, // @
        'suggests' => ConfigType::LIST_ARRAY, // @
        'artifact' => [self::class, 'validateArtifactField'], // STRING or OBJECT
        'license' => [ConfigType::class, 'validateLicenseField'],
        'lang' => ConfigType::STRING,
        'frameworks' => ConfigType::LIST_ARRAY, // @

        // php-extension type fields
        'php-extension' => ConfigType::ASSOC_ARRAY,
        'zend-extension' => ConfigType::BOOL,
        'support' => ConfigType::ASSOC_ARRAY,
        'arg-type' => ConfigType::STRING,
        'build-shared' => ConfigType::BOOL,
        'build-static' => ConfigType::BOOL,
        'build-with-php' => ConfigType::BOOL,
        'notes' => ConfigType::BOOL,
        'display-name' => ConfigType::STRING,
        'os' => ConfigType::LIST_ARRAY,

        // library and target fields
        'headers' => ConfigType::LIST_ARRAY, // @
        'static-libs' => ConfigType::LIST_ARRAY, // @
        'pkg-configs' => ConfigType::LIST_ARRAY,
        'static-bins' => ConfigType::LIST_ARRAY, // @
        'path' => ConfigType::LIST_ARRAY, // @
        'env' => ConfigType::ASSOC_ARRAY, // @
        'append-env' => ConfigType::ASSOC_ARRAY, // @
    ];

    public const array PACKAGE_FIELDS = [
        'type' => true,
        'description' => false,
        'depends' => false, // @
        'suggests' => false, // @
        'artifact' => false,
        'license' => false,
        'lang' => false,
        'frameworks' => false, // @

        // php-extension type fields
        'php-extension' => false,

        // library and target fields
        'headers' => false, // @
        'static-libs' => false, // @
        'pkg-configs' => false,
        'static-bins' => false, // @
        'path' => false, // @
        'env' => false, // @
        'append-env' => false, // @
    ];

    public const array SUFFIX_ALLOWED_FIELDS = [
        'depends',
        'suggests',
        'headers',
        'static-libs',
        'static-bins',
        'path',
        'env',
        'append-env',
    ];

    public const array PHP_EXTENSION_FIELDS = [
        'zend-extension' => false,
        'support' => false,
        'arg-type' => false, // @
        'build-shared' => false,
        'build-static' => false,
        'build-with-php' => false,
        'notes' => false,
        'display-name' => false,
        'os' => false,
    ];

    public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields]
        'filelist' => [['url', 'regex'], ['extract']],
        'git' => [['url'], ['extract', 'submodules', 'rev', 'regex']],
        'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match', 'query']],
        'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match', 'query']],
        'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable', 'query']],
        'url' => [['url'], ['filename', 'extract', 'version']],
        'bitbuckettag' => [['repo'], ['extract']],
        'local' => [['dirname'], ['extract']],
        'pie' => [['repo'], ['extract']],
        'pecl' => [['name'], ['extract', 'prefer-stable']],
        'php-release' => [['domain'], ['extract']],
        'custom' => [[], ['func']],
    ];

    /**
     * Validate and standardize artifacts configuration data.
     *
     * @param string $config_file_name Name of the configuration file (for error messages)
     * @param mixed  $data             The configuration data to validate
     */
    public static function validateAndLintArtifacts(string $config_file_name, mixed &$data): void
    {
        if (!is_array($data)) {
            throw new ValidationException("{$config_file_name} is broken");
        }

        // Define allowed artifact fields
        $allowed_artifact_fields = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata'];

        foreach ($data as $name => $artifact) {
            // First pass: validate unknown fields
            self::validateNoInvalidFields('artifact', $name, $artifact, $allowed_artifact_fields);

            foreach ($artifact as $k => $v) {
                // check source field
                if ($k === 'source' || $k === 'source-mirror') {
                    // source === custom is allowed
                    if ($v === 'custom') {
                        continue;
                    }
                    // expand string to url type (start with http:// or https://)
                    if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) {
                        $data[$name][$k] = [
                            'type' => 'url',
                            'url' => $v,
                        ];
                        continue;
                    }
                    // source: object with type field
                    if (is_assoc_array($v)) {
                        self::validateArtifactObjectField($name, $v);
                    }
                    continue;
                }
                // check binary field
                if ($k === 'binary') {
                    // binary === custom is allowed
                    if ($v === 'custom') {
                        $data[$name][$k] = [
                            'linux-x86_64' => ['type' => 'custom'],
                            'linux-aarch64' => ['type' => 'custom'],
                            'windows-x86_64' => ['type' => 'custom'],
                            'macos-x86_64' => ['type' => 'custom'],
                            'macos-aarch64' => ['type' => 'custom'],
                        ];
                        continue;
                    }
                    if ($v === 'hosted') {
                        $data[$name][$k] = [
                            'linux-x86_64' => ['type' => 'hosted'],
                            'linux-aarch64' => ['type' => 'hosted'],
                            'windows-x86_64' => ['type' => 'hosted'],
                            'macos-x86_64' => ['type' => 'hosted'],
                            'macos-aarch64' => ['type' => 'hosted'],
                        ];
                        continue;
                    }
                    if (is_assoc_array($v)) {
                        foreach ($v as $platform => $v_obj) {
                            self::validatePlatformString($platform);
                            // expand string to url type (start with http:// or https://)
                            if (is_string($v_obj) && (str_starts_with($v_obj, 'http://') || str_starts_with($v_obj, 'https://'))) {
                                $data[$name][$k][$platform] = [
                                    'type' => 'url',
                                    'url' => $v_obj,
                                ];
                                continue;
                            }
                            // binary: object with type field
                            if (is_assoc_array($v_obj)) {
                                self::validateArtifactObjectField("{$name}::{$platform}", $v_obj);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Validate packages configuration data.
     *
     * @param string $config_file_name Name of the configuration file (for error messages)
     * @param mixed  $data             The configuration data to validate
     */
    public static function validateAndLintPackages(string $config_file_name, mixed &$data): void
    {
        if (!is_array($data)) {
            throw new ValidationException("{$config_file_name} is broken");
        }
        foreach ($data as $name => $pkg) {
            if (!is_assoc_array($pkg)) {
                throw new ValidationException("Package [{$name}] in {$config_file_name} is not a valid associative array");
            }
            // check if package has valid type
            if (!isset($pkg['type']) || !in_array($pkg['type'], ConfigType::PACKAGE_TYPES)) {
                throw new ValidationException("Package [{$name}] in {$config_file_name} has invalid or missing 'type' field");
            }

            // validate basic fields using unified method
            self::validatePackageFields($name, $pkg);

            // validate list of suffix-allowed fields
            $suffixes = ['', '@windows', '@unix', '@macos', '@linux'];
            $fields = self::SUFFIX_ALLOWED_FIELDS;
            self::validateSuffixAllowedFields($name, $pkg, $fields, $suffixes);

            // check if "library|target" package has artifact field for target and library types
            if (in_array($pkg['type'], ['target', 'library']) && !isset($pkg['artifact'])) {
                throw new ValidationException("Package [{$name}] in {$config_file_name} of type '{$pkg['type']}' must have an 'artifact' field");
            }

            // validate and lint inline artifact object if present
            if (isset($pkg['artifact']) && is_array($pkg['artifact'])) {
                self::validateAndLintInlineArtifact($name, $data[$name]['artifact']);
            }

            // check if "php-extension" package has php-extension specific fields and validate inside
            if ($pkg['type'] === 'php-extension') {
                self::validatePhpExtensionFields($name, $pkg);
            }

            // check for unknown fields
            self::validateNoInvalidFields('package', $name, $pkg, array_keys(self::PACKAGE_FIELD_TYPES));
        }
    }

    /**
     * Validate platform string format.
     *
     * @param string $platform Platform string, like windows-x86_64
     */
    public static function validatePlatformString(string $platform): void
    {
        $valid_platforms = ['windows', 'linux', 'macos'];
        $valid_arch = ['x86_64', 'aarch64'];
        $parts = explode('-', $platform);
        if (count($parts) !== 2) {
            throw new ValidationException("Invalid platform format '{$platform}', expected format 'os-arch'");
        }
        [$os, $arch] = $parts;
        if (!in_array($os, $valid_platforms)) {
            throw new ValidationException("Invalid platform OS '{$os}' in platform '{$platform}'");
        }
        if (!in_array($arch, $valid_arch)) {
            throw new ValidationException("Invalid platform architecture '{$arch}' in platform '{$platform}'");
        }
    }

    /**
     * Validate artifact field - can be string (reference) or object (inline).
     *
     * @param mixed $value Field value
     */
    public static function validateArtifactField(mixed $value): bool
    {
        if (!is_string($value) && !is_assoc_array($value)) {
            return false;
        }
        return true;
    }

    /**
     * Validate an artifact download object field.
     *
     * @param string $item_name Artifact name (for error messages)
     * @param array  $data      Artifact source object data
     */
    private static function validateArtifactObjectField(string $item_name, array $data): void
    {
        if (!isset($data['type']) || !is_string($data['type'])) {
            throw new ValidationException("Artifact source object must have a valid 'type' field");
        }
        $type = $data['type'];
        if (!isset(self::ARTIFACT_TYPE_FIELDS[$type])) {
            throw new ValidationException("Artifact source object has unknown type '{$type}'");
        }
        [$required_fields, $optional_fields] = self::ARTIFACT_TYPE_FIELDS[$type];
        // check required fields
        foreach ($required_fields as $field) {
            if (!isset($data[$field])) {
                throw new ValidationException("Artifact source object of type '{$type}' must have required field '{$field}'");
            }
        }
        // check for unknown fields
        $allowed_fields = array_merge(['type'], $required_fields, $optional_fields);
        self::validateNoInvalidFields('artifact object', $item_name, $data, $allowed_fields);
    }

    /**
     * Unified method to validate config fields based on field definitions
     *
     * @param string $package_name Package name
     * @param mixed  $pkg          The package configuration array
     */
    private static function validatePackageFields(string $package_name, mixed $pkg): void
    {
        foreach (self::PACKAGE_FIELDS as $field => $required) {
            if ($required && !isset($pkg[$field])) {
                throw new ValidationException("Package {$package_name} must have [{$field}] field");
            }

            if (isset($pkg[$field])) {
                self::validatePackageFieldType($field, $pkg[$field], $package_name);
            }
        }
    }

    /**
     * Validate a field based on its global type definition
     *
     * @param string $field        Field name
     * @param mixed  $value        Field value
     * @param string $package_name Package name (for error messages)
     */
    private static function validatePackageFieldType(string $field, mixed $value, string $package_name): void
    {
        // Check if field exists in FIELD_TYPES
        if (!isset(self::PACKAGE_FIELD_TYPES[$field])) {
            // Try to strip suffix and check base field name
            $suffixes = ['@windows', '@unix', '@macos', '@linux'];
            $base_field = $field;
            foreach ($suffixes as $suffix) {
                if (str_ends_with($field, $suffix)) {
                    $base_field = substr($field, 0, -strlen($suffix));
                    break;
                }
            }

            if (!isset(self::PACKAGE_FIELD_TYPES[$base_field])) {
                // Unknown field is not allowed - strict validation
                throw new ValidationException("Package {$package_name} has unknown field [{$field}]");
            }

            // Use base field type for validation
            $expected_type = self::PACKAGE_FIELD_TYPES[$base_field];
        } else {
            $expected_type = self::PACKAGE_FIELD_TYPES[$field];
        }

        match ($expected_type) {
            ConfigType::STRING => is_string($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be string"),
            ConfigType::BOOL => is_bool($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be boolean"),
            ConfigType::LIST_ARRAY => is_list_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be a list"),
            ConfigType::ASSOC_ARRAY => is_assoc_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be an object"),
            default => $expected_type($value) ?: throw new ValidationException("Package {$package_name} [{$field}] has invalid type specification"),
        };
    }

    /**
     * Validate that fields with suffixes are list arrays
     */
    private static function validateSuffixAllowedFields(int|string $name, mixed $item, array $fields, array $suffixes): void
    {
        foreach ($fields as $field) {
            foreach ($suffixes as $suffix) {
                $key = $field . $suffix;
                if (isset($item[$key])) {
                    self::validatePackageFieldType($key, $item[$key], $name);
                }
            }
        }
    }

    /**
     * Validate php-extension specific fields for php-extension package
     */
    private static function validatePhpExtensionFields(int|string $name, mixed $pkg): void
    {
        if (!isset($pkg['php-extension'])) {
            return;
        }
        if (!is_assoc_array($pkg['php-extension'])) {
            throw new ValidationException("Package {$name} [php-extension] must be an object");
        }
        foreach (self::PHP_EXTENSION_FIELDS as $field => $required) {
            if (isset($pkg['php-extension'][$field])) {
                self::validatePackageFieldType($field, $pkg['php-extension'][$field], $name);
            }
        }
        // check for unknown fields in php-extension
        self::validateNoInvalidFields('php-extension', $name, $pkg['php-extension'], array_keys(self::PHP_EXTENSION_FIELDS));
    }

    private static function validateNoInvalidFields(string $config_type, int|string $item_name, mixed $item_content, array $allowed_fields): void
    {
        foreach ($item_content as $k => $v) {
            // remove suffixes for checking
            $base_k = $k;
            $suffixes = ['@windows', '@unix', '@macos', '@linux'];
            foreach ($suffixes as $suffix) {
                if (str_ends_with($k, $suffix)) {
                    $base_k = substr($k, 0, -strlen($suffix));
                    break;
                }
            }
            if (!in_array($base_k, $allowed_fields)) {
                throw new ValidationException("{$config_type} [{$item_name}] has invalid field [{$base_k}]");
            }
        }
    }

    /**
     * Validate and lint inline artifact object structure.
     *
     * @param string $pkg_name Package name
     * @param array  $artifact Inline artifact configuration (passed by reference to apply linting)
     */
    private static function validateAndLintInlineArtifact(string $pkg_name, array &$artifact): void
    {
        // Validate and lint as if it's a standalone artifact
        $temp_data = [$pkg_name => $artifact];
        self::validateAndLintArtifacts("inline artifact in package '{$pkg_name}'", $temp_data);
        // Write back the linted artifact configuration
        $artifact = $temp_data[$pkg_name];
    }
}
