<?php

namespace App\Models\Rule;

use App\Models\BaseModel;

/**
 * RuleDef Model
 * 
 * Rule definitions that can be linked to multiple tests via testrule mapping table.
 */
class RuleDefModel extends BaseModel
{
    protected $table = 'ruledef';
    protected $primaryKey = 'RuleID';
    protected $allowedFields = [
        'RuleCode',
        'RuleName',
        'Description',
        'EventCode',
        'ConditionExpr',
        'ConditionExprCompiled',
        'CreateDate',
        'StartDate',
        'EndDate',
    ];

    protected $useTimestamps = true;
    protected $createdField = 'CreateDate';
    protected $updatedField = 'StartDate';

    protected $useSoftDeletes = true;
    protected $deletedField = 'EndDate';

    /**
     * Fetch active rules for an event scoped by TestSiteID.
     *
     * Rules are standalone and only apply when explicitly linked to a test
     * via the testrule mapping table.
     *
     * @param string $eventCode The event code to filter by
     * @param int|null $testSiteID The test site ID to filter by
     * @return array Array of matching rules
     */
    public function getActiveByEvent(string $eventCode, ?int $testSiteID = null): array
    {
        if ($testSiteID === null) {
            return [];
        }

        return $this->select('ruledef.*')
            ->join('testrule', 'testrule.RuleID = ruledef.RuleID', 'inner')
            ->where('ruledef.EventCode', $eventCode)
            ->where('ruledef.EndDate IS NULL')
            ->where('testrule.TestSiteID', $testSiteID)
            ->where('testrule.EndDate IS NULL')
            ->orderBy('ruledef.RuleID', 'ASC')
            ->findAll();
    }

    /**
     * Get all tests linked to a rule
     *
     * @param int $ruleID The rule ID
     * @return array Array of test site IDs
     */
    public function getLinkedTests(int $ruleID): array
    {
        $db = \Config\Database::connect();
        $result = $db->table('testrule')
            ->where('RuleID', $ruleID)
            ->where('EndDate IS NULL')
            ->select('TestSiteID')
            ->get()
            ->getResultArray();
        
        return array_map('intval', array_column($result, 'TestSiteID'));
    }

    /**
     * Link a rule to a test
     *
     * @param int $ruleID The rule ID
     * @param int $testSiteID The test site ID
     * @return bool Success status
     */
    public function linkTest(int $ruleID, int $testSiteID): bool
    {
        $db = \Config\Database::connect();
        
        // Check if already linked (and not soft deleted)
        $existing = $db->table('testrule')
            ->where('RuleID', $ruleID)
            ->where('TestSiteID', $testSiteID)
            ->where('EndDate IS NULL')
            ->get()
            ->getRowArray();
        
        if ($existing) {
            return true; // Already linked
        }
        
        // Check if soft deleted - restore it
        $softDeleted = $db->table('testrule')
            ->where('RuleID', $ruleID)
            ->where('TestSiteID', $testSiteID)
            ->where('EndDate IS NOT NULL')
            ->get()
            ->getRowArray();
        
        if ($softDeleted) {
            return $db->table('testrule')
                ->where('TestRuleID', $softDeleted['TestRuleID'])
                ->update(['EndDate' => null]);
        }
        
        // Create new link
        return $db->table('testrule')->insert([
            'RuleID' => $ruleID,
            'TestSiteID' => $testSiteID,
            'CreateDate' => date('Y-m-d H:i:s'),
        ]);
    }

    /**
     * Unlink a rule from a test (soft delete)
     *
     * @param int $ruleID The rule ID
     * @param int $testSiteID The test site ID
     * @return bool Success status
     */
    public function unlinkTest(int $ruleID, int $testSiteID): bool
    {
        $db = \Config\Database::connect();
        
        return $db->table('testrule')
            ->where('RuleID', $ruleID)
            ->where('TestSiteID', $testSiteID)
            ->where('EndDate IS NULL')
            ->update(['EndDate' => date('Y-m-d H:i:s')]);
    }

    /**
     * Validate that a DSL expression compiles successfully
     *
     * @param string $expr The DSL expression to validate
     * @return array ['valid' => bool, 'error' => string|null, 'compiled' => array|null]
     */
    public function validateExpression(string $expr): array
    {
        $service = new \App\Services\RuleExpressionService();
        
        try {
            $compiled = $service->compile($expr);
            return [
                'valid' => true,
                'error' => null,
                'compiled' => $compiled,
            ];
        } catch (\Throwable $e) {
            return [
                'valid' => false,
                'error' => $e->getMessage(),
                'compiled' => null,
            ];
        }
    }

    /**
     * Compile a DSL expression to JSON
     *
     * @param string $expr The DSL expression to compile
     * @return array The compiled structure
     * @throws \InvalidArgumentException If compilation fails
     */
    public function compileExpression(string $expr): array
    {
        $service = new \App\Services\RuleExpressionService();
        return $service->compile($expr);
    }

    /**
     * Check if a rule's expression needs recompilation
     * Returns true if the raw expression differs from the compiled version
     *
     * @param int $ruleID The rule ID to check
     * @return bool True if recompilation is needed
     */
    public function needsRecompilation(int $ruleID): bool
    {
        $rule = $this->find($ruleID);
        if (!$rule) {
            return false;
        }

        $rawExpr = $rule['ConditionExpr'] ?? '';
        $compiledJson = $rule['ConditionExprCompiled'] ?? '';

        if (empty($rawExpr)) {
            return false;
        }

        if (empty($compiledJson)) {
            return true;
        }

        // Try to compile current expression and compare with stored version
        try {
            $compiled = $this->compileExpression($rawExpr);
            $stored = json_decode($compiledJson, true);
            
            return json_encode($compiled) !== json_encode($stored);
        } catch (\Throwable $e) {
            return true;
        }
    }

    /**
     * Automatically compile expression if needed and update the rule
     *
     * @param int $ruleID The rule ID
     * @return bool Success status
     */
    public function autoCompile(int $ruleID): bool
    {
        if (!$this->needsRecompilation($ruleID)) {
            return true;
        }

        $rule = $this->find($ruleID);
        if (!$rule) {
            return false;
        }

        $rawExpr = $rule['ConditionExpr'] ?? '';
        if (empty($rawExpr)) {
            return false;
        }

        try {
            $compiled = $this->compileExpression($rawExpr);
            return $this->update($ruleID, [
                'ConditionExprCompiled' => json_encode($compiled),
            ]);
        } catch (\Throwable $e) {
            log_message('error', 'Auto-compile failed for RuleID=' . $ruleID . ': ' . $e->getMessage());
            return false;
        }
    }

    /**
     * Compile all rules that need recompilation
     *
     * @return array ['success' => int, 'failed' => int]
     */
    public function compileAllPending(): array
    {
        $rules = $this->where('EndDate IS NULL')->findAll();
        $success = 0;
        $failed = 0;

        foreach ($rules as $rule) {
            $ruleID = (int) ($rule['RuleID'] ?? 0);
            if ($ruleID === 0) {
                continue;
            }

            if ($this->needsRecompilation($ruleID)) {
                if ($this->autoCompile($ruleID)) {
                    $success++;
                } else {
                    $failed++;
                }
            }
        }

        return [
            'success' => $success,
            'failed' => $failed,
        ];
    }
}
