<?php

declare(strict_types=1);

require_once __DIR__ . '/Database.php';

/**
 * SmartApply - Mapping Engine
 *
 * Responsible for converting SmartApply application fields
 * into the fields/values expected by an external application portal.
 */
final class MappingEngine
{
    private PDO $pdo;

    /**
     * Constructor
     *
     * If a PDO connection is supplied, use it.
     * Otherwise use Database::connection().
     */
    public function __construct(?PDO $pdo = null)
    {
        try {

            /*
             * If PDO was supplied directly, use it.
             */
            if ($pdo instanceof PDO) {
                $this->pdo = $pdo;
                return;
            }

            /*
             * SmartApply Database.php provides:
             *
             * Database::connection(): PDO
             *
             * It is a static method, therefore we must NOT do:
             *
             * new Database()->getConnection()
             */
            if (!class_exists('Database')) {
                throw new RuntimeException(
                    'Database class was not found. ' .
                    'Expected: Core/Database.php'
                );
            }

            if (!method_exists('Database', 'connection')) {
                throw new RuntimeException(
                    'Database::connection() method was not found.'
                );
            }

            $connection = Database::connection();

            if (!$connection instanceof PDO) {
                throw new RuntimeException(
                    'Database::connection() did not return a valid PDO connection.'
                );
            }

            $this->pdo = $connection;

        } catch (Throwable $e) {

            throw new RuntimeException(
                'MappingEngine could not obtain a valid PDO connection. ' .
                'Database error: ' . $e->getMessage(),
                0,
                $e
            );
        }
    }

    /**
     * Get all mappings for a portal.
     */
    public function getMappings(
        string $portalName,
        bool $activeOnly = true
    ): array {

        $sql = "
            SELECT
                id,
                university_id,
                portal_name,
                source_field,
                source_table,
                portal_field,
                portal_field_type,
                portal_value,
                transform_type,
                dependency_field,
                dependency_value,
                requires_postback,
                postback_target,
                postback_argument,
                is_required,
                is_active,
                mapping_order,
                description,
                created_at,
                updated_at
            FROM application_mappings
            WHERE portal_name = :portal_name
        ";

        if ($activeOnly) {
            $sql .= " AND is_active = 1 ";
        }

        $sql .= "
            ORDER BY mapping_order ASC, id ASC
        ";

        $stmt = $this->pdo->prepare($sql);

        $stmt->execute([
            ':portal_name' => $portalName
        ]);

        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Get mappings for one source field.
     */
    public function getFieldMappings(
        string $portalName,
        string $sourceField,
        bool $activeOnly = true
    ): array {

        $sql = "
            SELECT *
            FROM application_mappings
            WHERE portal_name = :portal_name
              AND source_field = :source_field
        ";

        if ($activeOnly) {
            $sql .= " AND is_active = 1 ";
        }

        $sql .= "
            ORDER BY mapping_order ASC, id ASC
        ";

        $stmt = $this->pdo->prepare($sql);

        $stmt->execute([
            ':portal_name' => $portalName,
            ':source_field' => $sourceField
        ]);

        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Find the mapping that applies to a source value.
     */
    public function resolveMapping(
        string $portalName,
        string $sourceField,
        mixed $sourceValue,
        array $applicationData = []
    ): ?array {

        $mappings = $this->getFieldMappings(
            $portalName,
            $sourceField
        );

        if (!$mappings) {
            return null;
        }

        foreach ($mappings as $mapping) {

            if (!$this->dependencyMatches(
                $mapping,
                $applicationData
            )) {
                continue;
            }

            /*
             * Generic mapping.
             */
            if (
                $mapping['portal_value'] === null ||
                $mapping['portal_value'] === ''
            ) {
                return $mapping;
            }

            /*
             * Specific value mapping.
             */
            $normalizedSource = $this->normalizeValue(
                $sourceValue,
                (string)($mapping['transform_type'] ?? 'none')
            );

            $normalizedPortal = $this->normalizeValue(
                $mapping['portal_value'],
                (string)($mapping['transform_type'] ?? 'none')
            );

            if (
                $normalizedSource !== '' &&
                $normalizedSource === $normalizedPortal
            ) {
                return $mapping;
            }
        }

        return null;
    }

    /**
     * Build a complete mapped payload.
     */
    public function buildPayload(
        string $portalName,
        array $applicationData
    ): array {

        $mappings = $this->getMappings($portalName);

        $payload = [];

        foreach ($mappings as $mapping) {

            $sourceField = (string)$mapping['source_field'];

            /*
             * Source field not supplied.
             */
            if (!array_key_exists(
                $sourceField,
                $applicationData
            )) {
                continue;
            }

            /*
             * Check dependency.
             */
            if (!$this->dependencyMatches(
                $mapping,
                $applicationData
            )) {
                continue;
            }

            $value = $applicationData[$sourceField];

            /*
             * Ignore empty optional values.
             */
            if (
                ($value === null || $value === '') &&
                (int)$mapping['is_required'] === 0
            ) {
                continue;
            }

            /*
             * Apply transformation.
             */
            $value = $this->transformValue(
                $value,
                (string)($mapping['transform_type'] ?? 'none')
            );

            /*
             * For mappings having a portal value,
             * convert the application value to the portal value.
             */
            if (
                $mapping['portal_value'] !== null &&
                $mapping['portal_value'] !== ''
            ) {

                $portalValue = $this->normalizeValue(
                    $mapping['portal_value'],
                    (string)($mapping['transform_type'] ?? 'none')
                );

                $normalizedValue = $this->normalizeValue(
                    $value,
                    (string)($mapping['transform_type'] ?? 'none')
                );

                if (
                    $normalizedValue !== '' &&
                    $normalizedValue === $portalValue
                ) {
                    $value = $mapping['portal_value'];
                }
            }

            $payload[] = [
                'mapping_id' => (int)$mapping['id'],

                'source_field' => $sourceField,

                'portal_field' => $mapping['portal_field'],

                'portal_field_type' =>
                    $mapping['portal_field_type'],

                'value' => $value,

                'requires_postback' =>
                    (bool)$mapping['requires_postback'],

                'postback_target' =>
                    $mapping['postback_target'],

                'postback_argument' =>
                    $mapping['postback_argument'],

                'is_required' =>
                    (bool)$mapping['is_required'],

                'mapping_order' =>
                    (int)$mapping['mapping_order'],

                'description' =>
                    $mapping['description']
            ];
        }

        return $payload;
    }

    /**
     * Validate required fields before automation.
     */
    public function validate(
        string $portalName,
        array $applicationData
    ): array {

        $mappings = $this->getMappings($portalName);

        $errors = [];

        foreach ($mappings as $mapping) {

            if ((int)$mapping['is_required'] !== 1) {
                continue;
            }

            if (!$this->dependencyMatches(
                $mapping,
                $applicationData
            )) {
                continue;
            }

            $sourceField = (string)$mapping['source_field'];

            if (
                !array_key_exists(
                    $sourceField,
                    $applicationData
                )
            ) {

                $errors[] = [
                    'source_field' => $sourceField,
                    'message' =>
                        "Required field '{$sourceField}' is missing."
                ];

                continue;
            }

            $value = $applicationData[$sourceField];

            if (
                $value === null ||
                (
                    is_string($value) &&
                    trim($value) === ''
                )
            ) {

                $errors[] = [
                    'source_field' => $sourceField,
                    'message' =>
                        "Required field '{$sourceField}' is empty."
                ];
            }
        }

        return $errors;
    }

    /**
     * Check mapping dependency.
     */
    private function dependencyMatches(
        array $mapping,
        array $applicationData
    ): bool {

        $dependencyField = trim(
            (string)(
                $mapping['dependency_field'] ?? ''
            )
        );

        $dependencyValue = trim(
            (string)(
                $mapping['dependency_value'] ?? ''
            )
        );

        /*
         * No dependency.
         */
        if ($dependencyField === '') {
            return true;
        }

        /*
         * Dependency exists but application does not contain it.
         */
        if (!array_key_exists(
            $dependencyField,
            $applicationData
        )) {
            return false;
        }

        $actualValue = $this->normalizeValue(
            $applicationData[$dependencyField],
            'trim'
        );

        $expectedValue = $this->normalizeValue(
            $dependencyValue,
            'trim'
        );

        return $actualValue === $expectedValue;
    }

    /**
     * Transform a value according to mapping configuration.
     */
    public function transformValue(
        mixed $value,
        string $transformType
    ): mixed {

        if ($value === null) {
            return null;
        }

        switch ($transformType) {

            case 'trim':

                return is_string($value)
                    ? trim($value)
                    : $value;

            case 'uppercase':

                return is_string($value)
                    ? strtoupper(trim($value))
                    : $value;

            case 'lowercase':

                return is_string($value)
                    ? strtolower(trim($value))
                    : $value;

            case 'country_code':

                return $this->normalizeCountryCode(
                    (string)$value
                );

            case 'phone':

                return $this->normalizePhone(
                    (string)$value
                );

            case 'date':

                return $this->normalizeDate(
                    (string)$value
                );

            case 'custom':

                return is_string($value)
                    ? trim($value)
                    : $value;

            case 'none':

            default:

                return $value;
        }
    }

    /**
     * Normalize values for comparisons.
     */
    public function normalizeValue(
        mixed $value,
        string $transformType = 'none'
    ): string {

        if ($value === null) {
            return '';
        }

        $value = (string)$value;

        /*
         * IMPORTANT:
         *
         * Portal country values can look like:
         *
         * "RW        "
         *
         * Normalize them to:
         *
         * "RW"
         */
        $value = trim($value);

        switch ($transformType) {

            case 'trim':

                return trim($value);

            case 'uppercase':

                return strtoupper($value);

            case 'lowercase':

                return strtolower($value);

            case 'country_code':

                return strtoupper(
                    trim($value)
                );

            case 'phone':

                return preg_replace(
                    '/[^0-9+]/',
                    '',
                    $value
                ) ?? '';

            default:

                return $value;
        }
    }

    /**
     * Normalize country code.
     */
    private function normalizeCountryCode(
        string $country
    ): string {

        $country = trim($country);

        /*
         * If it already looks like ISO code,
         * return normalized two-letter code.
         */
        if (
            strlen($country) === 2 &&
            ctype_alpha($country)
        ) {
            return strtoupper($country);
        }

        /*
         * Country-name -> ISO code.
         */
        $countries = [

            'rwanda' => 'RW',

            'uganda' => 'UG',

            'burundi' => 'BI',

            'tanzania' => 'TZ',

            'tanzania, united republic of' => 'TZ',

            'kenya' => 'KE',

            'south sudan' => 'SS',

            'democratic republic of the congo' => 'CD',

            'congo' => 'CG',

            'nigeria' => 'NG',

            'ghana' => 'GH',

            'south africa' => 'ZA',

            'zambia' => 'ZM',

            'zimbabwe' => 'ZW',

            'malawi' => 'MW',

            'mozambique' => 'MZ',

            'angola' => 'AO',

            'burkina faso' => 'BF',

            'cameroon' => 'CM',

            'ethiopia' => 'ET',

            'somalia' => 'SO',

            'senegal' => 'SN',

            'sierra leone' => 'SL',

            'togo' => 'TG',

            'benin' => 'BJ',

            'botswana' => 'BW',

            'namibia' => 'NA',

            'united states' => 'US',

            'united states minor outlying islands' => 'UM',

            'canada' => 'CA',

            'united kingdom' => 'GB',

            'belize' => 'BZ',

            'belarus' => 'BY',

            'united arab emirates' => 'AE',

            'saudi arabia' => 'SA',

            'india' => 'IN',

            'china' => 'CN',

            'japan' => 'JP',

            'south korea' => 'KR',

            'republic of korea' => 'KR',

            'germany' => 'DE',

            'france' => 'FR',

            'belgium' => 'BE',

            'italy' => 'IT',

            'spain' => 'ES',

            'austria' => 'AT',

            'switzerland' => 'CH',

            'netherlands' => 'NL',

            'portugal' => 'PT',

            'australia' => 'AU',

            'new zealand' => 'NZ',

            'brazil' => 'BR',

            'mexico' => 'MX',

            'argentina' => 'AR'
        ];

        $key = strtolower($country);

        return $countries[$key] ?? $country;
    }

    /**
     * Normalize phone numbers.
     */
    private function normalizePhone(
        string $phone
    ): string {

        $phone = trim($phone);

        /*
         * Keep digits and a possible leading +.
         */
        $phone = preg_replace(
            '/(?!^\+)[^0-9]/',
            '',
            $phone
        ) ?? '';

        return $phone;
    }

    /**
     * Normalize dates.
     *
     * Output:
     *
     * YYYY-MM-DD
     */
    private function normalizeDate(
        string $date
    ): string {

        $date = trim($date);

        if ($date === '') {
            return '';
        }

        $formats = [
            'Y-m-d',
            'd/m/Y',
            'd-m-Y',
            'm/d/Y',
            'm-d-Y'
        ];

        foreach ($formats as $format) {

            $dt = DateTime::createFromFormat(
                $format,
                $date
            );

            if (
                $dt instanceof DateTime &&
                $dt->format($format) === $date
            ) {
                return $dt->format('Y-m-d');
            }
        }

        return $date;
    }

    /**
     * Get all mappings grouped by source field.
     */
    public function getGroupedMappings(
        string $portalName
    ): array {

        $mappings = $this->getMappings(
            $portalName
        );

        $grouped = [];

        foreach ($mappings as $mapping) {

            $field = (string)$mapping['source_field'];

            if (!isset($grouped[$field])) {
                $grouped[$field] = [];
            }

            $grouped[$field][] = $mapping;
        }

        return $grouped;
    }

    /**
     * Get postback mappings only.
     */
    public function getPostbackMappings(
        string $portalName
    ): array {

        $mappings = $this->getMappings(
            $portalName
        );

        return array_values(
            array_filter(
                $mappings,
                static function (array $mapping): bool {

                    return (int)$mapping['requires_postback'] === 1;
                }
            )
        );
    }

    /**
     * Get mappings for a specific dependency.
     *
     * Example:
     *
     * faculty = FLAW
     *
     * returns programmes belonging to FLAW.
     */
    public function getDependentMappings(
        string $portalName,
        string $dependencyField,
        string $dependencyValue
    ): array {

        $sql = "
            SELECT *
            FROM application_mappings
            WHERE portal_name = :portal_name
              AND dependency_field = :dependency_field
              AND dependency_value = :dependency_value
              AND is_active = 1
            ORDER BY mapping_order ASC, id ASC
        ";

        $stmt = $this->pdo->prepare($sql);

        $stmt->execute([
            ':portal_name' => $portalName,
            ':dependency_field' => $dependencyField,
            ':dependency_value' => $dependencyValue
        ]);

        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Test whether the database connection is alive.
     */
    public function testConnection(): bool
    {
        try {

            $stmt = $this->pdo->query('SELECT 1');

            return $stmt !== false;

        } catch (Throwable) {

            return false;
        }
    }

    /**
     * Return the PDO connection.
     *
     * Useful for advanced automation components.
     */
    public function getConnection(): PDO
    {
        return $this->pdo;
    }
}
