<?php
declare(strict_types=1);

final class PolicyAutomator
{
    private mysqli $db;

    public function __construct(mysqli $db)
    {
        $this->db = $db;
    }

    /**
     * Build a read-only component allocation plan from questionnaire answers.
     * No legacy payroll row is written by this method.
     *
     * @param array<string,mixed> $answers
     * @param int[] $payrollLocationIds
     * @return array<string,mixed>
     */
    public function preview(array $answers, array $payrollLocationIds): array
    {
        $answers = $this->normalizeAnswers($answers);
        $payrollLocationIds = array_values(array_unique(array_filter(
            array_map('intval', $payrollLocationIds),
            static fn (int $id): bool => $id > 0
        )));

        $matched = [];
        $recipeKeys = [];
        foreach ($this->loadDecisions() as $decision) {
            if (!$this->matches((array)$decision['conditions'], $answers)) {
                continue;
            }
            $matched[] = [
                'decision_key' => $decision['decision_key'],
                'policy_module' => $decision['policy_module'],
                'priority' => $decision['priority'],
            ];
            foreach ((array)($decision['outcomes']['recipes'] ?? []) as $recipeKey) {
                $recipeKeys[(string)$recipeKey] = true;
            }
        }

        $recipes = $this->loadRecipes(array_keys($recipeKeys));
        $expanded = [];
        $errors = [];
        foreach (array_keys($recipeKeys) as $recipeKey) {
            $this->expandRecipe($recipeKey, $recipes, $expanded, [], $errors);
        }

        $catalog = $this->loadCatalog(array_values(array_unique(array_map(
            static fn (array $recipe): string => (string)$recipe['output_key'],
            $expanded
        ))));
        foreach ($expanded as $recipeKey => $recipe) {
            foreach ((array)$recipe['inputs'] as $input) {
                if (($input['input_kind'] ?? '') !== 'component') continue;
                $dependency = $this->findRecipeDependency((string)$input['input_key'], $expanded);
                if (!$dependency) continue;
                $dependencyCatalog = $catalog[(string)$dependency['output_key']] ?? null;
                $expectedUnit = (string)($input['expected_unit'] ?? '');
                $actualUnit = (string)($dependencyCatalog['unit'] ?? '');
                if ($expectedUnit !== '' && $actualUnit !== '' && $expectedUnit !== $actualUnit) {
                    $errors[] = "Unit mismatch in {$recipeKey}: expected {$expectedUnit}, received {$actualUnit}.";
                }
            }
        }
        $tenantMaps = $this->loadTenantMaps(array_keys($catalog), $payrollLocationIds);

        $actions = [];
        foreach ($payrollLocationIds as $locationId) {
            foreach ($expanded as $recipeKey => $recipe) {
                $outputKey = (string)$recipe['output_key'];
                $item = $catalog[$outputKey] ?? null;
                $mapKey = $locationId . ':' . $outputKey;
                $mappedId = (int)($tenantMaps[$mapKey]['salary_component_id'] ?? 0);
                $status = 'create';
                $reason = 'No component is mapped for this payroll location.';

                if (!$item) {
                    $status = 'blocked';
                    $reason = 'The component is missing from the approved catalog.';
                } elseif (($item['review_status'] ?? '') !== 'verified') {
                    $status = 'blocked';
                    $reason = 'The component definition has not passed review.';
                } elseif ($mappedId > 0) {
                    $status = 'reuse';
                    $reason = 'An approved component mapping already exists.';
                }

                $actions[] = [
                    'payroll_location_id' => $locationId,
                    'recipe_key' => $recipeKey,
                    'semantic_key' => $outputKey,
                    'component_name' => $item['name'] ?? $outputKey,
                    'unit' => $item['unit'] ?? '',
                    'operation' => $recipe['operation'],
                    'salary_component_id' => $mappedId ?: null,
                    'action' => $status,
                    'reason' => $reason,
                ];
            }
        }

        if (!$payrollLocationIds) {
            $errors[] = 'Choose at least one payroll location.';
        }
        if (!$matched) {
            $errors[] = 'No approved automation decision matches these answers.';
        }

        $blocked = count(array_filter($actions, static fn (array $row): bool => $row['action'] === 'blocked'));
        return [
            'mode' => 'dry_run',
            'answers' => $answers,
            'payroll_location_ids' => $payrollLocationIds,
            'matched_decisions' => $matched,
            'actions' => $actions,
            'errors' => array_values(array_unique($errors)),
            'can_activate' => !$errors && $blocked === 0,
            'summary' => [
                'decisions' => count($matched),
                'components' => count($actions),
                'create' => count(array_filter($actions, static fn (array $row): bool => $row['action'] === 'create')),
                'reuse' => count(array_filter($actions, static fn (array $row): bool => $row['action'] === 'reuse')),
                'blocked' => $blocked,
            ],
        ];
    }

    /** @param array<string,mixed> $answers @return array<string,mixed> */
    private function normalizeAnswers(array $answers): array
    {
        $normalized = [];
        foreach ($answers as $key => $value) {
            $key = strtolower(trim((string)$key));
            if ($key === '') continue;
            if (is_string($value)) {
                $trimmed = trim($value);
                $lower = strtolower($trimmed);
                if (in_array($lower, ['yes', 'true', 'on'], true)) $value = true;
                elseif (in_array($lower, ['no', 'false', 'off'], true)) $value = false;
                elseif (is_numeric($trimmed)) $value = strpos($trimmed, '.') !== false ? (float)$trimmed : (int)$trimmed;
                else $value = $lower;
            }
            $normalized[$key] = $value;
        }
        return $normalized;
    }

    /** @param array<string,mixed> $condition @param array<string,mixed> $answers */
    private function matches(array $condition, array $answers): bool
    {
        if (isset($condition['all'])) {
            foreach ((array)$condition['all'] as $child) {
                if (!$this->matches((array)$child, $answers)) return false;
            }
            return true;
        }
        if (isset($condition['any'])) {
            foreach ((array)$condition['any'] as $child) {
                if ($this->matches((array)$child, $answers)) return true;
            }
            return false;
        }
        $field = strtolower((string)($condition['field'] ?? ''));
        $operator = (string)($condition['operator'] ?? 'equals');
        $expected = $condition['value'] ?? null;
        $actual = $answers[$field] ?? null;
        switch ($operator) {
            case 'equals': return $actual === $expected;
            case 'not_equals': return $actual !== $expected;
            case 'in': return in_array($actual, (array)$expected, true);
            case 'truthy': return (bool)$actual === true;
            case 'present': return array_key_exists($field, $answers);
            case 'gt': return is_numeric($actual) && $actual > $expected;
            case 'gte': return is_numeric($actual) && $actual >= $expected;
            default: return false;
        }
    }

    /** @return array<int,array<string,mixed>> */
    private function loadDecisions(): array
    {
        $rows = [];
        $result = $this->db->query("SELECT decision_key, policy_module, conditions_json, outcomes_json, priority FROM jos_app_policy_kb_decision WHERE active=1 ORDER BY priority ASC, id ASC");
        if (!$result) return $rows;
        while ($row = $result->fetch_assoc()) {
            $row['conditions'] = json_decode((string)$row['conditions_json'], true) ?: [];
            $row['outcomes'] = json_decode((string)$row['outcomes_json'], true) ?: [];
            $rows[] = $row;
        }
        return $rows;
    }

    /** @param string[] $keys @return array<string,array<string,mixed>> */
    private function loadRecipes(array $keys): array
    {
        if (!$keys) return [];
        $recipes = [];
        $sql = "SELECT r.id,r.recipe_key,r.operation,r.parameters_json,c.semantic_key AS output_key FROM jos_app_component_kb_recipe r JOIN jos_app_component_kb_catalog c ON c.id=r.output_catalog_id WHERE r.active=1 AND c.active=1 ORDER BY r.version DESC";
        $result = $this->db->query($sql);
        if (!$result) return [];
        while ($row = $result->fetch_assoc()) {
            $key = (string)$row['recipe_key'];
            if (isset($recipes[$key])) continue;
            $row['parameters'] = json_decode((string)$row['parameters_json'], true) ?: [];
            $row['inputs'] = [];
            $recipes[$key] = $row;
        }
        if (!$recipes) return [];
        $result = $this->db->query("SELECT recipe_id,input_kind,input_key,expected_unit,operator_role,ordering FROM jos_app_component_kb_recipe_input ORDER BY recipe_id,ordering,id");
        if ($result) {
            $idToKey = [];
            foreach ($recipes as $key => $recipe) $idToKey[(int)$recipe['id']] = $key;
            while ($row = $result->fetch_assoc()) {
                $recipeKey = $idToKey[(int)$row['recipe_id']] ?? null;
                if ($recipeKey !== null) $recipes[$recipeKey]['inputs'][] = $row;
            }
        }
        return $recipes;
    }

    /** @param array<string,array<string,mixed>> $recipes @param array<string,array<string,mixed>> $expanded @param string[] $path @param string[] $errors */
    private function expandRecipe(string $recipeKey, array $recipes, array &$expanded, array $path, array &$errors): void
    {
        if (isset($expanded[$recipeKey])) return;
        if (in_array($recipeKey, $path, true)) {
            $errors[] = 'Component dependency cycle: ' . implode(' -> ', array_merge($path, [$recipeKey]));
            return;
        }
        if (!isset($recipes[$recipeKey])) {
            $errors[] = 'Required recipe is missing: ' . $recipeKey;
            return;
        }
        $path[] = $recipeKey;
        foreach ((array)$recipes[$recipeKey]['inputs'] as $input) {
            if (($input['input_kind'] ?? '') !== 'component') continue;
            $dependency = (string)$input['input_key'];
            $dependencyRecipeKey = isset($recipes[$dependency]) ? $dependency : null;
            if ($dependencyRecipeKey === null) {
                foreach ($recipes as $candidateKey => $candidate) {
                    if ((string)$candidate['output_key'] === $dependency) {
                        $dependencyRecipeKey = $candidateKey;
                        break;
                    }
                }
            }
            if ($dependencyRecipeKey !== null) {
                $this->expandRecipe($dependencyRecipeKey, $recipes, $expanded, $path, $errors);
            } else {
                $errors[] = 'Required component recipe is missing: ' . $dependency;
            }
        }
        $expanded[$recipeKey] = $recipes[$recipeKey];
    }

    /** @param array<string,array<string,mixed>> $recipes @return array<string,mixed>|null */
    private function findRecipeDependency(string $key, array $recipes): ?array
    {
        if (isset($recipes[$key])) return $recipes[$key];
        foreach ($recipes as $recipe) {
            if ((string)$recipe['output_key'] === $key) return $recipe;
        }
        return null;
    }

    /** @param string[] $keys @return array<string,array<string,mixed>> */
    private function loadCatalog(array $keys): array
    {
        if (!$keys) return [];
        $wanted = array_fill_keys($keys, true);
        $rows = [];
        $result = $this->db->query("SELECT id,semantic_key,name,module,role,unit,engine_output_key,legacy_function_name,review_status FROM jos_app_component_kb_catalog WHERE active=1 ORDER BY version DESC");
        if (!$result) return [];
        while ($row = $result->fetch_assoc()) {
            $key = (string)$row['semantic_key'];
            if (isset($wanted[$key]) && !isset($rows[$key])) $rows[$key] = $row;
        }
        return $rows;
    }

    /** @param string[] $semanticKeys @param int[] $locationIds @return array<string,array<string,mixed>> */
    private function loadTenantMaps(array $semanticKeys, array $locationIds): array
    {
        if (!$semanticKeys || !$locationIds) return [];
        $wantedKeys = array_fill_keys($semanticKeys, true);
        $wantedLocations = array_fill_keys($locationIds, true);
        $rows = [];
        $sql = "SELECT m.payroll_location_id,m.salary_component_id,c.semantic_key FROM jos_app_component_kb_tenant_map m JOIN jos_app_component_kb_catalog c ON c.id=m.catalog_id WHERE m.active=1 AND c.active=1";
        $result = $this->db->query($sql);
        if (!$result) return [];
        while ($row = $result->fetch_assoc()) {
            $locationId = (int)$row['payroll_location_id'];
            $key = (string)$row['semantic_key'];
            if (isset($wantedLocations[$locationId], $wantedKeys[$key])) $rows[$locationId . ':' . $key] = $row;
        }
        return $rows;
    }
}
