<?php
declare(strict_types=1);

/**
 * Whale-Patterns (Institutionelle Signale) – Score 0..100
 * - patterns_evaluate($symbol, $opts): prüft mehrere Kurs/Volumen-Patterns
 *   Rückgabe kompatibel zur Abfrage-Seite: ['score', 'signals', ...]
 */

if (!function_exists('fmp_multi')) {
	require_once __DIR__ . '/../api-handling/fmp_client.php';
}

/* ---------------- Einstellungen ---------------- */

const WP_DEFAULT_WEIGHTS = [
	'three_weeks_tight'   => 20,
	'flat_base'           => 15,
	'cup_with_handle'     => 20,
	'high_tight_flag'     => 15,
	'pocket_pivot'        => 10,
	'accumulation_days'   => 7,
	'up_down_volume'      => 8,
	'low_volume_pullback' => 5,
	'distribution_days'   => 7,
	'relative_strength'   => 10,
];

/* ---------------- kleine Helfer ---------------- */

function wp_sma(array $arr, int $len): array {
	$n = count($arr); $out = array_fill(0,$n,null);
	if ($len<=0 || $n===0) return $out;
	$sum = 0.0;
	for ($i=0; $i<$n; $i++){
		$v = is_numeric($arr[$i]) ? (float)$arr[$i] : null;
		if ($v===null) { $out[$i]=null; continue; }
		$sum += $v;
		if ($i >= $len) $sum -= (float)$arr[$i-$len];
		if ($i >= $len-1) $out[$i] = $sum/$len;
	}
	return $out;
}

function wp_build_weekly_from_daily(array $daily): array {
	$weeks = [];
	foreach ($daily as $bar) {
		if (!isset($bar['date'],$bar['open'],$bar['high'],$bar['low'],$bar['close'])) continue;
		$d = new DateTime($bar['date']);
		$key = $d->format('o-\WW'); // ISO year-week
		if (!isset($weeks[$key])) {
			$weeks[$key] = [
				'start'=>$bar['date'],'end'=>$bar['date'],
				'open'=>(float)$bar['open'],'high'=>(float)$bar['high'],'low'=>(float)$bar['low'],'close'=>(float)$bar['close'],
				'volume'=> isset($bar['volume']) ? (int)$bar['volume'] : null,
				'count'=>1
			];
		} else {
			$w =& $weeks[$key];
			$w['end']   = $bar['date'];
			$w['high']  = max($w['high'], (float)$bar['high']);
			$w['low']   = min($w['low'],  (float)$bar['low']);
			$w['close'] = (float)$bar['close'];
			$w['count']++;
			if (isset($bar['volume'])) $w['volume'] = (int)($w['volume'] ?? 0) + (int)$bar['volume'];
		}
	}
	return array_values($weeks); // oldest->newest
}

/* ---------------- Pattern-Detektoren ---------------- */

function wp_detect_three_weeks_tight(array $weekly): array {
	$n = count($weekly);
	if ($n < 13) return ['ok'=>false,'score'=>0,'why'=>'too_few_weeks'];
	if ($weekly[$n-1]['count'] < 3) { array_pop($weekly); $n--; if ($n<13) return ['ok'=>false,'score'=>0]; }
	$w1=$weekly[$n-3]; $w2=$weekly[$n-2]; $w3=$weekly[$n-1];

	$cl = [$w1['close'],$w2['close'],$w3['close']];
	$base = (float)$cl[0];
	$maxDev = max(abs($cl[0]-$base)/$base, abs($cl[1]-$base)/$base, abs($cl[2]-$base)/$base);
	$tightOK = ($maxDev <= 0.02);

	$rng = [
		($w1['high']-$w1['low'])/max(1e-9,$w1['close']),
		($w2['high']-$w2['low'])/max(1e-9,$w2['close']),
		($w3['high']-$w3['low'])/max(1e-9,$w3['close']),
	];
	$rangeOK = (max($rng) <= 0.06);

	$closes = array_map(fn($w)=>$w['close'],$weekly);
	$sma10  = wp_sma($closes,10);
	$smaOK  = (!is_null($sma10[$n-1]) && $w3['close'] >= $sma10[$n-1]);

	$volOK = true;
	if (isset($w1['volume'],$w2['volume'],$w3['volume'])) {
		$v1=(int)$w1['volume']; $v2=(int)$w2['volume']; $v3=(int)$w3['volume'];
		$volOK = ($v3 <= $v2*1.05 && $v2 <= $v1*1.05);
	}

	$priorIdx = max(0,$n-1-10);
	$runUp = ($w3['close']/max(1e-9,$weekly[$priorIdx]['close']) - 1.0);
	$advanceOK = ($runUp >= 0.20);

	$ok = ($tightOK && $rangeOK && $smaOK && $volOK && $advanceOK);
	$score = ($tightOK?40:0)+($rangeOK?25:0)+($smaOK?25:0)+($advanceOK?10:0);
	$twthigh = max($w1['high'], $w2['high'], $w3['high']);
	return ['ok'=>$ok,'score'=>min(100,$score),
		'metrics'=>[
			'max_close_dev_pct' =>round($maxDev*100,2),
			'max_week_range_pct' =>round(max($rng)*100,2),
			'prior_runup_pct' =>round($runUp*100,2),
			'pivot_price' => $twthigh,
		]
	];
}

function wp_detect_flat_base(array $weekly): array {
	if (!$weekly) return ['ok'=>false,'score'=>0];
	if ($weekly[count($weekly)-1]['count']<3) array_pop($weekly);
	$n=count($weekly); if ($n<6) return ['ok'=>false,'score'=>0];
	$slice = array_slice($weekly,$n-6,6);
	$high = max(array_column($slice,'high'));
	$low  = min(array_column($slice,'low'));
	$depth = ($high/max(1e-9,$low) - 1.0);
	$cl = array_column($slice,'close');
	$tight = ((max($cl)-min($cl))/max(1e-9,min($cl)) <= 0.03);
	$sma10 = wp_sma(array_column($weekly,'close'),10);
	$above=0; for($i=$n-6;$i<$n;$i++){ if(!is_null($sma10[$i]) && $weekly[$i]['close']>=$sma10[$i]) $above++; }
	$smaOK = ($above>=4);
	$ok = ($depth<=0.15 && $tight && $smaOK);
	$score = ($depth<=0.15?45:0)+($tight?35:0)+($smaOK?20:0);
	return ['ok'=>$ok,'score'=>$score,'metrics'=>['depth_pct'=>round($depth*100,2)]];
}

function wp_detect_cup_with_handle(array $weekly): array {
	if (!$weekly) return ['ok'=>false,'score'=>0];
	// unvollständige aktuelle Woche entfernen
	if ($weekly[count($weekly)-1]['count'] < 3) array_pop($weekly);

	$n = count($weekly);
	if ($n < 12) return ['ok'=>false,'score'=>0];

	$cl = array_column($weekly,'close');

	// Peak vor der rechten Tassenwand in den letzten ~30 Wochen suchen
	$win = min(30, $n - 6);
	$peakIdx = null; $peakVal = -INF;
	for ($i = $n - $win - 6; $i < $n - 6; $i++) {
		if ($i < 0) continue;
		if ($cl[$i] > $peakVal) { $peakVal = $cl[$i]; $peakIdx = $i; }
	}
	if ($peakIdx === null) return ['ok'=>false,'score'=>0];

	// Tiefpunkt der Tasse ab Peak suchen
	$lowIdx = $peakIdx;
	for ($i = $peakIdx; $i < $n; $i++) {
		if ($cl[$i] < $cl[$lowIdx]) $lowIdx = $i;
	}
	$peak = $cl[$peakIdx];
	$low  = $cl[$lowIdx];

	$depth = 1 - $low / max(1e-9, $peak);
	if ($depth < 0.12 || $depth > 0.33) {
		return ['ok'=>false,'score'=>0,'metrics'=>['depth_pct'=>round($depth*100,2)]];
	}

	// Handle-Prüfung (max ~10% unter dem aktuellen Schluss)
	$rs         = $weekly[$n-1]['close'];
	$handleDepth = ($rs - $weekly[$n-1]['low']) / max(1e-9, $rs);
	$handleOK    = ($handleDepth <= 0.10);

	// ✨ Pivot: Hoch der (vermuteten) Handle-Zone – bevorzugt die letzten 3 Wochen,
	//           aber nicht vor dem Tassen-Tief (lowIdx), damit wir nicht die linke Wand erwischen.
	$handleStart = max($lowIdx + 1, $n - 3);
	$handleSlice = array_slice($weekly, $handleStart);
	if (!$handleSlice) $handleSlice = array_slice($weekly, -3); // Fallback
	$handleHigh = max(array_column($handleSlice, 'high'));

	$score = 70 + ($handleOK ? 30 : 0);

	return [
		'ok'    => true,
		'score' => $score,
		'metrics' => [
			'depth_pct'   => round($depth*100, 2),
			'handle_ok'   => $handleOK,
			'pivot_price' => $handleHigh,  // ✅ jetzt definiert
		]
	];
}

function wp_detect_high_tight_flag(array $weekly): array {
	if (!$weekly) return ['ok'=>false,'score'=>0];
	if ($weekly[count($weekly)-1]['count']<3) array_pop($weekly);
	$n=count($weekly); if ($n<14) return ['ok'=>false,'score'=>0];
	$base = $weekly[$n-8]['close'];
	$peak = max(array_column(array_slice($weekly,$n-8,8),'close'));
	$gain = $peak/max(1e-9,$base) - 1.0;
	$flag = array_slice($weekly,$n-5,5);
	$flagLow  = min(array_column($flag,'low'));
	$pull = 1 - $flagLow/max(1e-9,$peak);
	$volOK = true;
	if (isset($flag[0]['volume'])) {
		$vol = array_column($flag,'volume');
		$volOK = (end($vol) <= min($vol)*1.15);
	}
	$gainOK = ($gain>=1.0);
	$pullOK = ($pull<=0.25);
	$ok = ($gainOK && $pullOK && $volOK);
	$score = ($gainOK?60:0)+($pullOK?30:0)+($volOK?10:0);
	return ['ok'=>$ok,'score'=>$score,'metrics'=>['gain_pct'=>round($gain*100,2),'pull_pct'=>round($pull*100,2)]];
}

/* --- PATCH: Pocket Pivot strenger: 10-DMA (Close & Vol), 90-Tage-Fenster --- */
function wp_detect_pocket_pivot(array $daily): array {
	$n = count($daily); if ($n < 30) return ['ok'=>false,'score'=>0,'count'=>0];

	$close = array_map(fn($d)=>$d['close']??null, $daily);
	$vol   = array_map(fn($d)=>$d['volume']??null, $daily);
	$ma10c = wp_sma($close, 10);
	$ma10v = wp_sma($vol,   10);

	$start = max(11, $n - 90); // nur letzte 90 Tage scannen
	$count = 0; $lastDate = null;

	for ($i = $start; $i < $n; $i++) {
		$t = $daily[$i]; $y = $daily[$i-1] ?? null;
		if (!isset($t['close'],$t['volume'],$y['close'])) continue;
		if ($ma10c[$i] === null || $ma10v[$i] === null) continue;

		// Up-Day & Close über 10-DMA
		if (!($t['close'] > $y['close'] && $t['close'] > $ma10c[$i])) continue;

		// höchstes Down-Vol der letzten 10 Tage
		$maxDownVol = 0;
		for ($k = $i - 10; $k < $i; $k++) {
			if (!isset($daily[$k]['close'],$daily[$k-1]['close'],$daily[$k]['volume'])) continue;
			if ($daily[$k]['close'] < $daily[$k-1]['close']) $maxDownVol = max($maxDownVol, (int)$daily[$k]['volume']);
		}
		if ($maxDownVol <= 0) continue;

		// Volumen > max Down-Vol & > 10d-Vol-SMA * 1.05
		if ((int)$t['volume'] <= $maxDownVol) continue;
		if ((int)$t['volume'] <= (int)round(($ma10v[$i] ?? 0) * 1.05)) continue;

		$count++; $lastDate = $t['date'] ?? $lastDate;
	}

	$score = min(100, 60 + min(40, $count * 10));
	return ['ok' => $count > 0, 'score' => $score, 'count' => $count, 'lastDate' => $lastDate];
}

/* --- PATCH: Accumulation Days strenger + Anti-Clustering --- */
function wp_detect_accumulation_days(array $daily): array {
	$n = count($daily); if ($n < 20) return ['ok'=>false,'score'=>0,'count'=>0];

	$vol  = array_map(fn($d)=>$d['volume']??null, $daily);
	$ma10 = wp_sma($vol, 10);

	$start = max(1, $n - 50); // Fenster 50 Tage
	$candidates = [];

	for ($i = $start; $i < $n; $i++) {
		$t = $daily[$i]; $y = $daily[$i-1] ?? null;
		if (!isset($t['close'],$t['volume'],$y['close'],$y['volume'])) continue;
		if ($ma10[$i] === null) continue;

		$priceUp = ($t['close'] >= $y['close'] * 1.002); // ≥ +0.2 %
		$volUp   = ((int)$t['volume'] > (int)$y['volume']) && ((int)$t['volume'] > (int)($ma10[$i]));
		if ($priceUp && $volUp) $candidates[] = $i;
	}

	// Anti-Clustering: max 1 in jedem 3-Tage-Fenster
	$idx = []; $last = -999;
	foreach ($candidates as $i) {
		if ($i - $last >= 3) { $idx[] = $i; $last = $i; }
	}

	$cnt = count($idx);
	$lastDate = $cnt ? ($daily[end($idx)]['date'] ?? null) : null;
	$score = min(100, $cnt * 25);

	return ['ok' => $cnt >= 3, 'score' => $score, 'count' => $cnt, 'lastDate' => $lastDate];
}

function wp_detect_up_down_volume_ratio(array $daily): array {
	$n=count($daily); if ($n<51) return ['ok'=>false,'score'=>0,'ratio'=>null];
	$start = max(1,$n-50);
	$up=0; $down=0;
	for($i=$start;$i<$n;$i++){
		$t=$daily[$i]; $y=$daily[$i-1];
		if (!isset($t['close'],$y['close'],$t['volume'])) continue;
		if ($t['close'] >= $y['close']) $up += (int)$t['volume']; else $down += (int)$t['volume'];
	}
	if ($down==0) $down=1;
	$ratio = $up/$down;
	$ok = ($ratio >= 1.2);
	$score = (int)min(100, max(0, ($ratio-0.8)/0.4 * 100)); // 0 bei 0.8, 100 bei 1.2+
	return ['ok'=>$ok,'score'=>$score,'ratio'=>round($ratio,2)];
}

function wp_detect_low_volume_pullback(array $daily): array {
	$n=count($daily); if ($n<30) return ['ok'=>false,'score'=>0];
	$vol = array_map(fn($d)=>$d['volume'] ?? null, $daily);
	$volSMA10 = wp_sma($vol,10);
	$ok=false; $score=0; $last=null;
	for($i=20;$i<$n-1;$i++){
		for($len=3;$len<=10;$len++){
			$j=$i-$len+1; if($j<1) continue;
			$decline = ($daily[$i]['close'] < $daily[$j-1]['close']);
			if(!$decline) continue;
			$lowVolOK=true;
			for($k=$j;$k<=$i;$k++){
				if (!isset($volSMA10[$k]) || !isset($daily[$k]['volume'])) { $lowVolOK=false; break; }
				if ($daily[$k]['volume'] > $volSMA10[$k]) { $lowVolOK=false; break; }
			}
			if(!$lowVolOK) continue;
			$r=$i+1;
			if ($daily[$r]['close'] > $daily[$i]['close'] && (int)$daily[$r]['volume'] > (int)$daily[$i]['volume']) {
				$ok=true; $score=80; $last=$daily[$r]['date']??null; break 2;
			}
		}
	}
	return ['ok'=>$ok,'score'=>$score,'lastDate'=>$last];
}

/* --- Distribution Days (Spiegel zu Accumulation, 50T-Fenster + Anti-Clustering) --- */
function wp_detect_distribution_days(array $daily): array {
	$n = count($daily); if ($n < 20) return ['ok'=>true,'score'=>100,'count'=>0]; // kein Nachteil, wenn zu wenig Daten

	$vol  = array_map(fn($d)=>$d['volume']??null, $daily);
	$ma10 = wp_sma($vol, 10);

	$start = max(1, $n - 50);
	$candidates = [];
	for ($i = $start; $i < $n; $i++) {
		$t = $daily[$i]; $y = $daily[$i-1] ?? null;
		if (!isset($t['close'],$t['volume'],$y['close'],$y['volume'])) continue;
		if ($ma10[$i] === null) continue;

		$priceDown = ($t['close'] <= $y['close'] * 0.998); // ≤ -0,2 %
		$volUp     = ((int)$t['volume'] > (int)$y['volume']) && ((int)$t['volume'] > (int)$ma10[$i]);
		if ($priceDown && $volUp) $candidates[] = $i;
	}
	// Anti-Clustering: max 1 in jedem 3-Tage-Fenster
	$idx=[]; $last=-999;
	foreach ($candidates as $i) { if ($i - $last >= 3) { $idx[]=$i; $last=$i; } }

	$cnt = count($idx);
	$lastDate = $cnt ? ($daily[end($idx)]['date'] ?? null) : null;

	// Score invers: je mehr Distribution, desto schlechter (linear: -25 Punkte je Treffer)
	$score = max(0, 100 - $cnt*25);
	$ok = ($cnt <= 3); // <=3 innerhalb 50 Tage gilt als akzeptabel

	return ['ok'=>$ok,'score'=>$score,'count'=>$cnt,'lastDate'=>$lastDate];
}

/* --- Relative Stärke vs. Benchmark (z. B. SPY) ----------------------------------- */
function wp_detect_relative_strength(array $daily, array $benchDaily): array {
	// Mappe nach Datum und aligniere
	$px = []; foreach ($daily as $d)  if (!empty($d['date']))  $px[$d['date']]  = (float)$d['close'];
	$bx = []; foreach ($benchDaily as $d) if (!empty($d['date'])) $bx[$d['date']] = (float)$d['close'];

	$dates = array_values(array_intersect(array_keys($px), array_keys($bx)));
	sort($dates);
	if (count($dates) < 60) return ['ok'=>false,'score'=>0,'why'=>'too_few_aligned'];

	$rs = [];
	foreach ($dates as $dt) {
		$bc = $bx[$dt]; if ($bc == 0.0) { $rs[] = null; continue; }
		$rs[] = $px[$dt] / $bc;
	}
	// RS-SMA50 & 20-Tage-Veränderung der RS
	$rsSma50 = wp_sma($rs, 50);
	$n = count($rs);
	$rsNow = $rs[$n-1] ?? null;
	$rsSmaNow = $rsSma50[$n-1] ?? null;

	// 20-Tage RS-Change (%)
	$len = 20;
	if ($n <= $len) return ['ok'=>false,'score'=>0,'why'=>'too_few_rs'];
	$prev = $rs[$n-1-$len] ?? null;
	$chg = (is_numeric($rsNow) && is_numeric($prev) && $prev != 0.0) ? (($rsNow/$prev - 1.0) * 100.0) : null;

	$aboveSma = (is_numeric($rsNow) && is_numeric($rsSmaNow)) ? ($rsNow >= $rsSmaNow) : false;
	$chgOK    = (is_numeric($chg) ? ($chg >= 2.0) : false); // RS +2% in 20 Tagen

	// Score: 60 für RS>RS-SMA50, +0..40 für RS-Change (2%→+20, 5%→+40 gedeckelt)
	$chgScore = 0;
	if (is_numeric($chg)) {
		if ($chg >= 5.0) $chgScore = 40;
		elseif ($chg >= 2.0) $chgScore = (int)round(($chg - 2.0) / 3.0 * 20) + 20; // 2..5% → 20..40
	}
	$score = ($aboveSma ? 60 : 0) + $chgScore;
	$score = min(100, max(0, $score));

	return [
		'ok'      => ($aboveSma && $chgOK),
		'score'   => $score,
		'metrics' => [
			'rs_above_sma50' => $aboveSma,
			'rs_20d_change_pct' => is_numeric($chg) ? round($chg,2) : null,
		]
	];
}

/* ---------------- Kernfunktion: EIN Symbol ---------------- */

function patterns_evaluate(string $symbol, array $opts = []): array {
	$o = array_merge([
		'timeseries' => 420,
		'weights'    => WP_DEFAULT_WEIGHTS,
		'benchmark'  => 'SPY',
	], $opts);

	$symbol = strtoupper(trim($symbol));

	$multi = fmp_multi([
		'prices' => [
			'key'   => 'historical_price_full',
			'vars'  => ['symbol' => $symbol],
			'query' => ['timeseries' => max(220, (int)$o['timeseries'])],
		],
		'profile' => [
			'key'  => 'profile',
			'vars' => ['symbol' => $symbol],
		],
		'bench' => [
			'key'   => 'historical_price_full',
			'vars'  => ['symbol' => (string)$o['benchmark']],
			'query' => ['timeseries' => max(220, (int)$o['timeseries'])],
		],
	]);

	$hist = $multi['prices']['historical'] ?? ($multi['prices'] ?? null);
	if (!is_array($hist) || count($hist) < 120) {
		return ['error' => 'no_data', 'symbol' => $symbol];
	}

	usort($hist, fn($a,$b) => strcmp((string)($a['date'] ?? ''), (string)($b['date'] ?? '')));
	$hist = array_values($hist);

	$daily = [];
	foreach ($hist as $h) {
		if (!isset($h['open'],$h['high'],$h['low'],$h['close'])) continue;
		$daily[] = [
			'date'   => (string)($h['date'] ?? ''),
			'open'   => (float)$h['open'],
			'high'   => (float)$h['high'],
			'low'    => (float)$h['low'],
			'close'  => (float)$h['close'],
			'volume' => isset($h['volume']) ? (int)$h['volume'] : null,
		];
	}
	if (count($daily) < 120) return ['error'=>'no_data','symbol'=>$symbol];
	
	// Benchmark-Daily
	$benchHist = $multi['bench']['historical'] ?? ($multi['bench'] ?? null);
	$benchDaily = [];
	if (is_array($benchHist)) {
		usort($benchHist, fn($a,$b)=>strcmp((string)($a['date']??''),(string)($b['date']??'')));
		foreach ($benchHist as $h) {
			if (!isset($h['close'])) continue;
			$benchDaily[] = [
				'date'  => (string)($h['date'] ?? ''),
				'close' => (float)$h['close'],
			];
		}
	}

	$weekly = wp_build_weekly_from_daily($daily);

	$signals = [];
	$signals['three_weeks_tight']   = wp_detect_three_weeks_tight($weekly);
	$signals['flat_base']           = wp_detect_flat_base($weekly);
	$signals['cup_with_handle']     = wp_detect_cup_with_handle($weekly);
	$signals['high_tight_flag']     = wp_detect_high_tight_flag($weekly);
	$signals['pocket_pivot']        = wp_detect_pocket_pivot($daily);
	$signals['accumulation_days']   = wp_detect_accumulation_days($daily);
	$signals['up_down_volume']      = wp_detect_up_down_volume_ratio($daily);
	$signals['low_volume_pullback'] = wp_detect_low_volume_pullback($daily);
	$signals['distribution_days']   = wp_detect_distribution_days($daily);
	$signals['relative_strength']   = $benchDaily ? wp_detect_relative_strength($daily, $benchDaily)
												  : ['ok'=>false,'score'=>0,'why'=>'no_benchmark'];
	
	$pivotPrice = null;
	if (!empty($signals['cup_with_handle']['metrics']['pivot_price'])) {
		$pivotPrice = (float)$signals['cup_with_handle']['metrics']['pivot_price'];
	} elseif (!empty($signals['three_weeks_tight']['metrics']['pivot_price'])) {
		$pivotPrice = (float)$signals['three_weeks_tight']['metrics']['pivot_price'];
	}


	$weights = $o['weights'];
	$score = 0;
	foreach ($weights as $k=>$w) {
		$s = $signals[$k] ?? null;
		if (!$s) continue;
	
		if (isset($s['score']) && is_numeric($s['score'])) {
			$raw  = max(0, min(100, (int)$s['score']));         // 0..100
			$part = (int)round($raw * ((int)$w) / 100);         // 0..$w
		} else {
			$part = !empty($s['ok']) ? (int)$w : 0;             // Fallback
		}
		$score += $part;
	}
	$score = min(100, $score);

	$prof     = $multi['profile'][0] ?? ($multi['profile'] ?? []);
	$name     = $prof['companyName']       ?? $symbol;
	$exchange = $prof['exchangeShortName'] ?? ($prof['exchange'] ?? '');

	return [
		'symbol'   => $symbol,
		'name'     => $name,
		'exchange' => $exchange,
		'score'    => (int)$score,
		'signals'  => $signals,
		'weights'  => $weights,
		'pivot_price' => $pivotPrice,
	];
}