<?php
namespace App; // Asegúrate que el namespace coincida

use App\Trader; // O tu librería de indicadores
use InvalidArgumentException;

/**
 * Clase MarketDirectionSignal (Híbrida - Basada en SMMA/ZLEMA/md)
 *
 * Genera señales de trading usando:
 * 1. El indicador 'md' (calculado con SMMA/ZLEMA estilo PineScript).
 * 2. Cruces entre 'md' y su señal 'sb' (SMA de md).
 * 3. Umbrales estáticos (basados en 'md' histórico) para validar la fuerza de 'md'.
 * 4. Filtro de tendencia EMA.
 * 5. Filtro opcional de whipsaw/lateralidad basado en tamaño del histograma 'sh' (md - sb).
 */
class MarketDirectionSignal {
    //region Propiedades de Configuración
    /** @var int */
    private $lengthMA; // = 34 en PineScript
    /** @var int */
    private $lengthSignal; // = 9 en PineScript
    // --- Gestión de Riesgo y Tendencia ---
    /** @var int */
    private $atrPeriod;
    /** @var float */
    private $atrMultiplier;
    /** @var int */
    private $emaTrendPeriod;
    // --- Filtro Opcional de Whipsaw/Lateralidad ---
    /** @var float */
    private $histogramAbsThreshold; // Aplicado a 'sh'
    /** @var int */
    private $sidewaysLookback;
    //endregion

    //region Propiedades para Resultados Calculados
    // --- Componentes Indicador Principal (md/sb/sh) ---
    /** @var array|null */ private $hlc3;
    /** @var array|null */ private $hi; // smma(high)
    /** @var array|null */ private $lo; // smma(low)
    /** @var array|null */ private $mi; // zlema(hlc3)
    /** @var array|null */ private $md; // <-- Línea Principal (Market Direction)
    /** @var array|null */ private $sb; // <-- Línea de Señal (SMA de md)
    /** @var array|null */ private $sh; // <-- Histograma (md - sb)
    // --- Umbrales Estáticos (Basados en 'md' histórico) ---
    /** @var float */ private $staticUpperThreshold;
    /** @var float */ private $staticLowerThreshold;
    // --- Tendencia ---
    /** @var array|null */ private $emaTrend;
    //endregion

    //region Constructor
    public function __construct(
        int $lengthMA = 34,
        int $lengthSignal = 9,
        int $atrPeriod = 14,
        float $atrMultiplier = 2,
        int $emaTrendPeriod = 100,
        float $histogramAbsThreshold = 0.005, // Ajustado por ti (o 0 para desactivar)
        int $sidewaysLookback = 3
    ) {
        if ($lengthMA <= 0 || $lengthSignal <= 0 || $atrPeriod <= 0 || $emaTrendPeriod <= 0 || $histogramAbsThreshold < 0 || $sidewaysLookback < 0) {
             throw new InvalidArgumentException("Parámetros inválidos.");
        }
        $this->lengthMA = $lengthMA;
        $this->lengthSignal = $lengthSignal;
        $this->atrPeriod = $atrPeriod;
        $this->atrMultiplier = $atrMultiplier;
        $this->emaTrendPeriod = $emaTrendPeriod;
        $this->histogramAbsThreshold = $histogramAbsThreshold;
        $this->sidewaysLookback = $sidewaysLookback;
        // Inicializar resultados
        $this->hlc3 = null; $this->hi = null; $this->lo = null; $this->mi = null;
        $this->md = null; $this->sb = null; $this->sh = null;
        $this->staticUpperThreshold = 0.0; $this->staticLowerThreshold = 0.0;
        $this->emaTrend = null;
    }
    //endregion

    //region Método Principal de Cálculo
    /**
     * Calcula todos los componentes necesarios para la estrategia.
     */
    public function calculate(array $data): self {
        // 1. Validaciones
        if (!isset($data['high'], $data['low'], $data['close'])) { throw new InvalidArgumentException("Datos incompletos."); }
        $countData = count($data['close']);
        // ZLEMA necesita aprox 2*len + EMA_trend_period? Ser conservador.
        $minPoints = max($this->lengthMA * 2, $this->lengthSignal, $this->emaTrendPeriod, $this->atrPeriod) + 50; // Margen extra
        if ($countData < $minPoints) { throw new InvalidArgumentException("Datos insuficientes ({$countData} < {$minPoints})."); }

        // 2. Calcular hlc3
        $this->hlc3 = [];
        foreach ($data['close'] as $key => $closeValue) {
            if (isset($data['high'][$key], $data['low'][$key]) && is_numeric($closeValue) && is_numeric($data['high'][$key]) && is_numeric($data['low'][$key])) {
                $this->hlc3[$key] = ($data['high'][$key] + $data['low'][$key] + $closeValue) / 3.0;
            } else {
                $this->hlc3[$key] = null;
            }
        }
        $this->hlc3 = $this->cleanIndicatorOutput($this->hlc3); // Limpiar por si acaso

        // 3. Calcular hi, lo, mi usando SMMA y ZLEMA
        // Necesitamos pasar arrays con valores numéricos continuos a las funciones
        $highValues = array_values(array_filter($data['high'], 'is_numeric'));
        $lowValues = array_values(array_filter($data['low'], 'is_numeric'));
        $hlc3Values = array_values(array_filter($this->hlc3, 'is_numeric'));

        $hiResult = $this->calculateSMMA($highValues, $this->lengthMA);
        $loResult = $this->calculateSMMA($lowValues, $this->lengthMA);
        $miResult = $this->calculateZLEMA($hlc3Values, $this->lengthMA);

        // Alinear resultados con las claves originales (usando hlc3 como referencia ya que tiene las claves correctas)
        $this->hi = $this->alignAndCleanWithReference($hiResult, $this->hlc3);
        $this->lo = $this->alignAndCleanWithReference($loResult, $this->hlc3);
        $this->mi = $this->alignAndCleanWithReference($miResult, $this->hlc3);


        // 4. Calcular md
        $this->md = [];
        foreach ($this->mi as $key => $miValue) {
            $hiValue = $this->hi[$key] ?? null;
            $loValue = $this->lo[$key] ?? null;
            $mdValue = null; // Por defecto

            if (is_numeric($miValue) && is_numeric($hiValue) && is_numeric($loValue)) {
                if ($miValue > $hiValue) {
                    $mdValue = $miValue - $hiValue;
                } elseif ($miValue < $loValue) {
                    $mdValue = $miValue - $loValue;
                } else {
                    $mdValue = 0.0;
                }
            }
            $this->md[$key] = $mdValue;
        }
        // MD ya está alineado porque se basa en MI

        // 5. Calcular sb (SMA de md) y sh (md - sb)
        $mdValues = array_values(array_filter($this->md, 'is_numeric'));
        if (empty($mdValues)) { throw new InvalidArgumentException("No se pudieron calcular valores 'md' válidos.");}

        $sbResult = Trader::SMA($mdValues, $this->lengthSignal);
        $this->sb = $this->alignAndCleanWithReference($sbResult, $this->md); // Alinear con MD

        $this->sh = [];
        foreach ($this->md as $key => $mdValue) {
            $sbValue = $this->sb[$key] ?? null;
            $shValue = null;
            if (is_numeric($mdValue) && is_numeric($sbValue)) {
                $shValue = $mdValue - $sbValue;
            }
            $this->sh[$key] = $shValue;
        }
        // SH ya está alineado

        // 6. Calcular Umbrales Estáticos (basados en 'md')
        $this->calculateStaticThresholds(); // Ahora usa $this->md internamente

        // 7. Calcular EMA de Tendencia
        $this->emaTrend = $this->cleanIndicatorOutput(Trader::EMA($data['close'], $this->emaTrendPeriod));
        // Alinear EMA Trend con MD/SB/SH (importante si EMA tiene diferente offset inicial)
        $this->emaTrend = $this->alignAndCleanWithReference($this->emaTrend, $this->md);

        return $this;
    }
    //endregion

    //region --- Implementaciones Indicadores Personalizados ---
    /**
     * Calcula la Smoothed Moving Average (SMMA / RMA).
     * NOTA: Requiere un array con índices numéricos 0..N-1.
     */
    private function calculateSMMA(array $source, int $length): array {
        $output = [];
        $count = count($source);
        if ($count < $length) {
            return array_fill(0, $count, null); // No hay suficientes datos
        }

        // Calcular primer valor con SMA
        $initialSum = 0;
        for ($i = 0; $i < $length; $i++) {
            if (!is_numeric($source[$i])) return array_fill(0, $count, null); // Dato inválido inicial
            $initialSum += $source[$i];
        }
        $output[$length - 1] = $initialSum / $length;

        // Calcular valores restantes
        for ($i = $length; $i < $count; $i++) {
             if (!is_numeric($source[$i])) {
                 // Si un dato posterior es inválido, ¿continuamos con el último valor válido o ponemos null?
                 // Pondremos null para indicar discontinuidad.
                 $output[$i] = null;
                 continue;
             }
             // Si el valor anterior es null (por un dato inválido previo), no podemos continuar la fórmula recursiva
             if ($output[$i-1] === null) {
                  $output[$i] = null;
                  continue;
             }
             // Fórmula SMMA: smma = (smma[1] * (len - 1) + src) / len
             $output[$i] = ($output[$i - 1] * ($length - 1) + $source[$i]) / $length;
        }

        // Rellenar los nulos iniciales (antes del índice length-1)
        for ($i = 0; $i < $length - 1; $i++) {
            array_unshift($output, null);
        }
        // Asegurar que el tamaño final coincide con la fuente
        // Esto puede no ser necesario si la lógica anterior es correcta
         while (count($output) < $count) {
              array_unshift($output, null);
         }
         // O ajustar el tamaño final si se generaron más elementos (no debería ocurrir)
         if (count($output) > $count) {
            $output = array_slice($output, -$count);
         }


        return $output; // Devuelve array indexado numéricamente
    }

    /**
     * Calcula la Zero-Lag Exponential Moving Average (ZLEMA).
     * NOTA: Requiere un array con índices numéricos 0..N-1.
     */
    private function calculateZLEMA(array $source, int $length): array {
        $count = count($source);
        $ema1Result = Trader::EMA($source, $length);
        $ema1 = $this->cleanIndicatorOutput($ema1Result); // Limpiar y convertir a float/null

        // Si ema1 está vacío, no podemos continuar
        if (empty($ema1)) return array_fill(0, $count, null);

        // Necesitamos pasar valores numéricos a la segunda EMA, quitando nulos
        $ema1Values = array_values(array_filter($ema1, 'is_numeric'));
        if (empty($ema1Values)) return array_fill(0, $count, null);

        $ema2Result = Trader::EMA($ema1Values, $length);
        // Alinear ema2 con ema1 (que ya está alineado con source)
        $ema2 = $this->alignAndCleanWithReference($ema2Result, $ema1);


        $zlema = [];
        // Usar las claves de ema1 como referencia
        $ema1Keys = array_keys($ema1);
        foreach($ema1Keys as $key) {
            $e1 = $ema1[$key] ?? null;
            $e2 = $ema2[$key] ?? null;
            $zl = null;
            if (is_numeric($e1) && is_numeric($e2)) {
                $d = $e1 - $e2;
                $zl = $e1 + $d;
            }
            // Guardar con la clave original
             if (array_key_exists($key, $source)) { // Asegurar que la clave existe en la fuente original
                 $zlema[$key] = $zl;
             } else {
                 // Esto no debería pasar si ema1 está bien alineado, pero por seguridad
                 // podríamos añadirlo con un índice numérico si es necesario.
             }
        }
         // Reconstruir array indexado si es necesario, pero alignAndClean hará eso
         // Devolver array indexado 0..N-1 alineado con source
         $final_zlema = array_fill(0, $count, null);
         $zlemaValues = array_values($zlema);
         $zlemaCount = count($zlemaValues);
         $offset = $count - $zlemaCount;
         for ($i = 0; $i < $count; $i++) {
            if ($i >= $offset && isset($zlemaValues[$i-$offset])) {
               $final_zlema[$i] = $zlemaValues[$i-$offset];
            }
         }


        return $final_zlema; // Devuelve array indexado numéricamente
    }
    //endregion

    //region Cálculo Umbrales Estáticos (Basado en 'md')
    /**
     * Calcula los umbrales estáticos basados en la SMA histórica de 'md'.
     */
    private function calculateStaticThresholds(): void {
        if (!is_array($this->md) || empty($this->md)) { $this->staticUpperThreshold = 0.0; $this->staticLowerThreshold = 0.0; return; }
        $positiveMdValues = []; $negativeMdValues = [];
        foreach ($this->md as $value) { if (is_numeric($value)) { if ($value > 0) { $positiveMdValues[] = $value; } elseif ($value < 0) { $negativeMdValues[] = $value; } } }
        $posSmaResult = count($positiveMdValues) > 0 ? Trader::SMA(array_values($positiveMdValues), count($positiveMdValues)) : false;
        $negSmaResult = count($negativeMdValues) > 0 ? Trader::SMA(array_values($negativeMdValues), count($negativeMdValues)) : false;
        $this->staticUpperThreshold = ($posSmaResult && !empty($posSmaResult)) ? (float)end($posSmaResult) : 0.0;
        $this->staticLowerThreshold = ($negSmaResult && !empty($negSmaResult)) ? (float)end($negSmaResult) : 0.0;
    }
    //endregion

    //region Detección de Cruces (usa 'md' y 'sb')
    /** @param int|string $index @return bool */
    public function isBullishCrossMD($index): bool {
        // Necesitamos el índice *anterior*
        // Encontrar la clave anterior en el array puede ser complejo si no son numéricas
        // Asumiremos claves numéricas o que podemos obtener la clave anterior fácilmente
        $prevIndex = is_numeric($index) ? $index - 1 : null; // Simplificación
        if ($prevIndex === null || !isset($this->md[$index], $this->sb[$index], $this->md[$prevIndex], $this->sb[$prevIndex])) { return false; }
        if (!is_numeric($this->md[$index]) || !is_numeric($this->sb[$index]) || !is_numeric($this->md[$prevIndex]) || !is_numeric($this->sb[$prevIndex])) { return false; }
        return $this->md[$index] > $this->sb[$index] && $this->md[$prevIndex] <= $this->sb[$prevIndex];
    }
    /** @param int|string $index @return bool */
    public function isBearishCrossMD($index): bool {
         $prevIndex = is_numeric($index) ? $index - 1 : null; // Simplificación
         if ($prevIndex === null || !isset($this->md[$index], $this->sb[$index], $this->md[$prevIndex], $this->sb[$prevIndex])) { return false; }
         if (!is_numeric($this->md[$index]) || !is_numeric($this->sb[$index]) || !is_numeric($this->md[$prevIndex]) || !is_numeric($this->sb[$prevIndex])) { return false; }
        return $this->md[$index] < $this->sb[$index] && $this->md[$prevIndex] >= $this->sb[$prevIndex];
    }
    //endregion

    //region Método de Estrategia
     public function generateStrategy(array $data): array {
        // 1. Calcular
        try { $this->calculate($data); }
        catch (InvalidArgumentException $e) {
             return ['action' => 'HOLD', 'signalType' => 'CALC_ERROR', 'filter_reason' => 'Calc Error: ' . $e->getMessage(), /* ... otros campos default ... */];
        }

        // 2. Inicializar resultado
        $strategy = [
            'action' => 'HOLD', 'stopLoss' => 0.0, 'takeProfit' => 0.0, 'signalType' => '',
            'static_upper' => $this->staticUpperThreshold, 'static_lower' => $this->staticLowerThreshold,
            'md' => null, 'sb' => null, 'sh' => null, // <-- Valores del nuevo indicador
            'ema_trend' => null, 'price' => null,
            'is_bullish_cross' => false, 'is_bearish_cross' => false, 'filter_reason' => ''
        ];

        // 3. Validar datos último índice
        $lastIndex = $this->array_key_last($data['close']);
        $requiredIndicatorKeys = [$this->md, $this->sb, $this->sh, $this->emaTrend];
        $dataRequiredKeys = [$data['close'], $data['high'], $data['low']];
        $validLastIndexData = ($lastIndex !== null);
        if ($validLastIndexData) {
            foreach ($requiredIndicatorKeys as $indicatorArray) {
                 // Permitir que sh sea 0.0
                if (!isset($indicatorArray[$lastIndex]) || (!is_numeric($indicatorArray[$lastIndex]) && $indicatorArray[$lastIndex] !== null)) {
                     // Excepción para sh = 0.0 ? No, debe ser numérico o null.
                    $validLastIndexData = false; break;
                }
            }
            if ($validLastIndexData) { /* ... validar dataRequiredKeys ... */
                foreach ($dataRequiredKeys as $dataArray) {
                    if (!isset($dataArray[$lastIndex]) || !is_numeric($dataArray[$lastIndex])) {
                        $validLastIndexData = false; break;
                    }
                }
            }
        }
        if (!$validLastIndexData) {
            $strategy['filter_reason'] = 'Insufficient/invalid indicator or price data for the last point.'; return $strategy;
        }

        // 4. Poblar valores actuales
        $strategy['md'] = (float)$this->md[$lastIndex]; // <-- Valor MD
        $strategy['sb'] = (float)$this->sb[$lastIndex];
        $strategy['sh'] = (float)$this->sh[$lastIndex]; // <-- Histograma SH
        $strategy['ema_trend'] = (float)$this->emaTrend[$lastIndex];
        $strategy['price'] = (float)$data['close'][$lastIndex];
        $closePrice = $strategy['price'];

        // 5. Calcular Cruces (basados en MD vs SB)
        $isBullish = $this->isBullishCrossMD($lastIndex);
        $isBearish = $this->isBearishCrossMD($lastIndex);
        $strategy['is_bullish_cross'] = $isBullish;
        $strategy['is_bearish_cross'] = $isBearish;

        // 6. *** FILTRO OPCIONAL WHIPSAW (Aplicado a 'sh') ***
        $isWeakSignal = false;
        if ($this->histogramAbsThreshold > 0 && is_numeric($strategy['sh'])) { // Usa SH ahora
            $currentAbsHistogram = abs($strategy['sh']); // Usa SH
            if ($this->sidewaysLookback > 0) {
                 $consistentLowMomentum = true; $firstHistoKey = $this->array_key_first($this->sh); // Usa SH
                 $startIndex = null;
                 if ($firstHistoKey !== null && is_numeric($lastIndex) && is_numeric($firstHistoKey)) {
                     $numericStartIndex = max($firstHistoKey, $lastIndex - $this->sidewaysLookback + 1);
                     if(array_key_exists($numericStartIndex, $this->sh)) { $startIndex = $numericStartIndex; } // Usa SH
                 }
                 if ($startIndex !== null && $startIndex <= $lastIndex) {
                     for ($i = $startIndex; $i <= $lastIndex; $i++) {
                          if (!isset($this->sh[$i]) || !is_numeric($this->sh[$i]) || abs((float)$this->sh[$i]) >= $this->histogramAbsThreshold) { // Usa SH
                              $consistentLowMomentum = false; break;
                          }
                     }
                 } else { $consistentLowMomentum = false; }
                 if ($consistentLowMomentum) { $isWeakSignal = true; $strategy['filter_reason'] = "Weak Signal: 'sh' consistently small."; }
            } else { if ($currentAbsHistogram < $this->histogramAbsThreshold) { $isWeakSignal = true; $strategy['filter_reason'] = "Weak Signal: Current 'sh' too small."; } }
        }
        // *** FIN FILTRO OPCIONAL ***

        // 7. Determinar Acción Principal
        $action = 'HOLD'; $signalType = '';
        if ($isWeakSignal) {
            $action = 'HOLD'; $signalType = 'HISTOGRAM_FILTERED';
        } else {
            $mdValue = $strategy['md']; // <-- Usa MD
            $staticUpper = $strategy['static_upper']; // Calculado sobre MD
            $staticLower = $strategy['static_lower']; // Calculado sobre MD
            $priceAboveTrend = $closePrice > $strategy['ema_trend'];
            $priceBelowTrend = $closePrice < $strategy['ema_trend'];

            // Condiciones usando mdValue vs staticThresholds
            if ($isBullish && $priceAboveTrend && $mdValue > $staticUpper) { // No necesitamos $mdValue != 0 aquí
                 $action = 'BUY'; $signalType = 'BUY_CONFIRMED'; $strategy['filter_reason'] = 'Buy: BullishCrossMD, Price>EMA, MD>StaticUpper.';
            } elseif ($isBearish && $priceBelowTrend && $mdValue < $staticLower) { // No necesitamos $mdValue != 0 aquí
                 $action = 'SELL'; $signalType = 'SELL_CONFIRMED'; $strategy['filter_reason'] = 'Sell: BearishCrossMD, Price<EMA, MD<StaticLower.';
            } else {
                 $action = 'HOLD'; $signalType = 'NO_TRADE_CONDITION'; $strategy['filter_reason'] = $isWeakSignal ? $strategy['filter_reason'] : 'Conditions not met.';
            }
        }
        $strategy['action'] = $action; $strategy['signalType'] = $signalType;

        // 8. Calcular SL/TP (igual que antes, usa ATR y EMA Trend)
        if ($action === 'BUY' || $action === 'SELL') {
              $atrValues = Trader::ATR($data['high'], $data['low'], $data['close'], $this->atrPeriod);
              $atrValues = $this->cleanIndicatorOutput($atrValues);
              if (isset($atrValues[$lastIndex]) && is_numeric($atrValues[$lastIndex]) && $atrValues[$lastIndex] > 0) {
                   $currentAtr = (float)$atrValues[$lastIndex];
                   $strategy['stopLoss'] = ($action === 'BUY') ? $strategy['ema_trend'] - ($currentAtr * $this->atrMultiplier) : $strategy['ema_trend'] + ($currentAtr * $this->atrMultiplier);
                   $strategy['takeProfit'] = ($action === 'BUY') ? $closePrice + ($currentAtr * $this->atrMultiplier) : $closePrice - ($currentAtr * $this->atrMultiplier);
              } else {
                  $strategy['action'] = 'HOLD'; $strategy['signalType'] = 'ATR_CALC_FAILED'; $strategy['filter_reason'] .= ' ATR calculation failed.';
                  $strategy['stopLoss'] = 0.0; $strategy['takeProfit'] = 0.0;
              }
        } else { $strategy['stopLoss'] = 0.0; $strategy['takeProfit'] = 0.0; }

        // 9. Devolver resultado
        return $strategy;
     }
    //endregion

    //region Getters (Opcional)
    public function getStaticUpperThreshold(): float { return $this->staticUpperThreshold; }
    public function getStaticLowerThreshold(): float { return $this->staticLowerThreshold; }
    public function getMDArray(): ?array { return $this->md; }
    public function getSBArray(): ?array { return $this->sb; }
    public function getSHArray(): ?array { return $this->sh; }
    //endregion

    //region Métodos de Utilidad (Incluidos)
    private function cleanIndicatorOutput($indicatorOutput): array {
        if ($indicatorOutput === false || !is_array($indicatorOutput)) { return []; } $cleaned = [];
        foreach ($indicatorOutput as $key => $value) { if ($value === false) { $cleaned[$key] = null; } elseif (is_numeric($value)) { $cleaned[$key] = (float)$value; } else { $cleaned[$key] = null; } }
        return $cleaned;
    }
     private function alignAndCleanWithReference($resultArray, ?array $referenceArray): array {
        if ($resultArray === false || !is_array($resultArray) || $referenceArray === null || empty($referenceArray)) { return array_fill_keys(array_keys($referenceArray ?? []), null); }
        $cleanedResult = $this->cleanIndicatorOutput($resultArray); if (empty($cleanedResult)) { return array_fill_keys(array_keys($referenceArray), null); }
        $alignedArray = []; $refKeys = array_keys($referenceArray); $resValues = array_values($cleanedResult); $resCount = count($resValues); $refCount = count($refKeys); $offset = $refCount - $resCount;
        foreach ($refKeys as $index => $key) { $resIndex = $index - $offset; if ($resIndex >= 0 && isset($resValues[$resIndex])) { $alignedArray[$key] = $resValues[$resIndex]; } else { $alignedArray[$key] = null; } } return $alignedArray;
     }
    private function array_key_last(array $arr) { return empty($arr) ? null : array_key_last($arr); }
    private function array_key_first(array $arr) { return empty($arr) ? null : array_key_first($arr); }
    //endregion

} // Fin Clase MarketDirectionSignal (Híbrida)