<?php
declare(strict_types=1);
require_once __DIR__ . '/../../engine/api-handling/fmp_client.php';
header('Content-Type: application/json; charset=utf-8');

// ---------- Inputs ----------
$sector     = trim($_GET['sector']   ?? '');
$industry   = trim($_GET['industry'] ?? '');
$country    = trim($_GET['country']  ?? '');
$exchange   = trim($_GET['exchange'] ?? '');
$chunk      = max(50, min(200, (int)($_GET['chunk'] ?? 200)));

$useMedian  = (isset($_GET['agg']) && strtolower($_GET['agg']) === 'median'); // Anzeigezweck
$topK       = isset($_GET['top_k']) ? max(1, (int)$_GET['top_k']) : 10;

// Basics / Tuning
$minMcap    = isset($_GET['min_mcap'])     ? (float)$_GET['min_mcap']     : 1_000_000_000; // ≥ 1B
$minPrice   = isset($_GET['min_price'])    ? (float)$_GET['min_price']    : 5.0;           // ≥ $5
$qualityMax = isset($_GET['quality_max'])  ? max(50,(int)$_GET['quality_max']) : 300;      // max Symbole für Quality-Fetch
$coverageMin= isset($_GET['coverage_min']) ? (float)$_GET['coverage_min'] : 0.25;          // min Forward-Coverage
$MIN_GROUP  = 6; // ab 6 Titeln pro Sub-Industry perzentilieren; sonst Sektor-Perzentil

if ($sector === '') { http_response_code(400); echo json_encode(['error'=>'sector required']); exit; }

// --- A) Sektorspezifische Schwellen (Defaults) ---
$SECTOR_THRESHOLDS = [
  'technology'     => [ 'pe_floor' => 15.0, 'min_roic' => 12.0 ],
  'healthcare'     => [ 'pe_floor' => 10.0, 'min_roic' => 10.0 ],
  'financial'      => [ 'pe_floor' =>  6.0, 'min_roic' =>  8.0 ],
  'energy'         => [ 'pe_floor' =>  5.0, 'min_roic' =>  8.0 ],
  'materials'      => [ 'pe_floor' =>  8.0, 'min_roic' =>  8.0 ],
  'industrials'    => [ 'pe_floor' => 10.0, 'min_roic' => 10.0 ],
  'consumer'       => [ 'pe_floor' => 12.0, 'min_roic' => 10.0 ],
  'utilities'      => [ 'pe_floor' =>  8.0, 'min_roic' =>  6.0 ],
  'real estate'    => [ 'pe_floor' =>  8.0, 'min_roic' =>  6.0 ],
  'communication'  => [ 'pe_floor' => 10.0, 'min_roic' =>  8.0 ],
];

$sectorKey = strtolower($sector);
$sectorCfg = $SECTOR_THRESHOLDS[$sectorKey] ?? [ 'pe_floor'=>8.0, 'min_roic'=>8.0 ];

// Inputs dürfen die Defaults überschreiben (leer/fehlend => Default)
$peFloor = (isset($_GET['pe_floor']) && $_GET['pe_floor'] !== '')
  ? (float)$_GET['pe_floor']
  : (float)$sectorCfg['pe_floor'];

$minRoic = (isset($_GET['min_roic']) && $_GET['min_roic'] !== '')
  ? (float)$_GET['min_roic']
  : (float)$sectorCfg['min_roic'];

// ---------- Helpers ----------
function ci_contains(?string $hay, string $needle): bool {
  if ($needle === '') return true;
  return $hay !== null && stripos($hay, $needle) !== false;
}
function format_marketcap($val) {
  if (!is_numeric($val)) return null;
  $v = (float)$val;
  if ($v >= 1_000_000_000_000) return round($v/1_000_000_000_000,2).' T';
  if ($v >= 1_000_000_000)     return round($v/1_000_000_000,2).' B';
  if ($v >= 1_000_000)         return round($v/1_000_000,2).' M';
  if ($v >= 1_000)             return round($v/1_000,2).' K';
  return (string)$v;
}
function median(array $xs): ?float {
  $xs = array_values(array_filter($xs, 'is_numeric'));
  $n = count($xs); if ($n===0) return null;
  sort($xs, SORT_NUMERIC);
  $m = intdiv($n,2);
  return $n%2 ? (float)$xs[$m] : (($xs[$m-1]+$xs[$m])/2.0);
}
function mean(array $xs): ?float {
  $xs = array_values(array_filter($xs, 'is_numeric'));
  $n = count($xs); if ($n===0) return null;
  return array_sum($xs)/$n;
}
function rank_percentile_one(float $v, array $xs): ?float {
  $vals = array_values(array_filter($xs, 'is_numeric'));
  $n = count($vals); if ($n===0) return null;
  sort($vals, SORT_NUMERIC);
  if ($v <= $vals[0]) return 0.0;
  if ($v >= $vals[$n-1]) return 1.0;
  $L=0; $R=$n-1;
  while ($L+1 < $R) {
    $m = intdiv($L+$R, 2);
    if ($vals[$m] == $v) { $L=$m; $R=$m+1; break; }
    if ($vals[$m] < $v) $L=$m; else $R=$m;
  }
  $span = $vals[$R] - $vals[$L];
  $pos  = $L + ($span>0 ? ($v - $vals[$L])/$span : 0);
  return $pos / ($n-1);
}
function clamp01(?float $x): ?float {
  if ($x===null) return null;
  if ($x<0) return 0.0;
  if ($x>1) return 1.0;
  return $x;
}
function pick_first(array $row, array $candidates) {
  foreach ($candidates as $k) if (isset($row[$k]) && $row[$k] !== null) return $row[$k];
  return null;
}
// robust: erstes Row (verträgt [], [0=>..], ['x'=>..], {...})
function first_row($res): ?array {
  if (!is_array($res) || empty($res)) return null;
  if (isset($res['symbol'])) return $res;
  $firstKey = array_key_first($res);
  $first = $res[$firstKey];
  return is_array($first) ? $first : null;
}
// Gruppentools
function group_arrays_by(array $rows, string $key, string $field): array {
  $out = [];
  foreach ($rows as $r) {
    $g = $r[$key] ?? 'UNKNOWN';
    $out[$g][] = $r[$field] ?? null;
  }
  return $out;
}
function rank_in_group_or_sector($val, array $groupVals, array $sectorVals, int $minGroup): ?float {
  if (!is_numeric($val)) return null;
  $g = array_values(array_filter($groupVals, 'is_numeric'));
  $use = (count($g) >= $minGroup) ? $g : array_values(array_filter($sectorVals,'is_numeric'));
  $n = count($use); if ($n===0) return null;
  sort($use, SORT_NUMERIC);
  if ($val <= $use[0]) return 0.0;
  if ($val >= $use[$n-1]) return 1.0;
  $L=0; $R=$n-1;
  while ($L+1<$R) { $m=intdiv($L+$R,2); if ($use[$m] < $val) $L=$m; else $R=$m; }
  $span = $use[$R]-$use[$L];
  $pos  = $L + ($span>0 ? ($val-$use[$L])/$span : 0);
  return $pos/($n-1);
}

// ---------- 1) Screener ----------
$resp = fmp_multi([
  's' => ['key'=>'stock_screener','query'=>['sector'=>$sector,'isEtf'=>'false','limit'=>5000]]
], 20);
$list = is_array($resp['s'] ?? null) ? $resp['s'] : [];
$initialCount = count($list);

// Meta-Map
$meta = [];
foreach ($list as $r) {
  $sym = $r['symbol'] ?? null; if(!$sym) continue;
  $meta[$sym] = [
    'name'     => $r['companyName'] ?? ($r['companyName'] ?? null),
    'industry' => $r['industry'] ?? null,
    'country'  => $r['country'] ?? ($r['countryName'] ?? null),
    'exchange' => $r['exchangeShortName'] ?? ($r['exchange'] ?? null),
  ];
}
$symbols = array_keys($meta);

// ---------- 2) Lokaler Filter ----------
$symbols = array_values(array_filter($symbols, function($s) use($meta,$industry,$country,$exchange){
  $m = $meta[$s] ?? [];
  return ci_contains($m['industry'] ?? '', $industry)
      && ci_contains($m['country']  ?? '', $country)
      && ci_contains($m['exchange'] ?? '', $exchange);
}));
$afterLocal = count($symbols);

if ($afterLocal === 0) {
  echo json_encode([
    'mode'    => 'ttm',
    'sector'  => $sector,
    'filters' => ['industry'=>$industry?:null,'country'=>$country?:null,'exchange'=>$exchange?:null],
    'counts'  => ['screener'=>$initialCount,'after_local'=>0,'quotes'=>0,'filtered'=>0],
    'averages'=> ['pe'=>null,'eps'=>null],
    'items'   => [],
    'items_all'=>[]
  ], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);
  exit;
}

// --- Exclusion-Keywords (optional) ---
$exclude = trim($_GET['exclude'] ?? '');
if ($exclude !== '') {
  $needles = array_filter(array_map('trim', explode(',', strtolower($exclude))));
  $symbols = array_values(array_filter($symbols, function($sym) use($meta,$needles){
    $m = $meta[$sym] ?? [];
    $hay = strtolower(
        $sym.' '.
        ($m['symbol'] ?? '').' '.
        ($m['name'] ?? '').' '.
        ($m['industry'] ?? '').' '.
        ($m['sector'] ?? '').' '.
        ($m['exchange'] ?? '').' '.
        ($m['description'] ?? '')
    );
    foreach ($needles as $n) {
      if ($n !== '' && strpos($hay, $n) !== false) {
        return false;
      }
    }
    return true;
  }));
  $afterLocal = count($symbols);
}

// ---------- 3) Quotes ----------
$req = [];
for ($i=0; $i<count($symbols); $i += $chunk) {
  $req["q$i"] = ['key'=>'quote','vars'=>['symbol'=>implode(',', array_slice($symbols, $i, $chunk))]];
}
$rq = fmp_multi($req, 20);
$quotes = [];
foreach ($rq as $arr) if (is_array($arr)) foreach ($arr as $q) {
  $s = $q['symbol'] ?? null; if(!$s) continue; $quotes[$s] = $q;
}
$quotesCount = count($quotes);

// ---------- 4) Analyst Estimates (Forward, optional) ----------
$forwardAvailable = false;
$estBySym = [];
try {
  $probeSym = $symbols[0] ?? null;
  if ($probeSym) {
    $probe = fmp_multi([
      'e' => ['key'=>'analyst_estimates','vars'=>['symbol'=>$probeSym],'query'=>['limit'=>1]]
    ], 5);
    $forwardAvailable = is_array($probe['e'] ?? null) && count($probe['e'])>0;
  }
  if ($forwardAvailable) {
    $reqE = [];
    foreach ($symbols as $idx=>$sym) {
      $reqE["e$idx"] = ['key'=>'analyst_estimates','vars'=>['symbol'=>$sym],'query'=>['limit'=>1]];
      if (count($reqE) === 80) {
        $batch = fmp_multi($reqE, 20);
        foreach ($batch as $rows) {
          $row = first_row($rows);
          if (!$row) continue;
          $symRow = $row['symbol'] ?? $sym;
          $estBySym[$symRow] = $row;
        }
        $reqE = [];
      }
    }
    if ($reqE) {
      $batch = fmp_multi($reqE, 20);
      foreach ($batch as $rows) {
        $row = first_row($rows);
        if (!$row) continue;
        $symRow = $row['symbol'] ?? array_key_first($estBySym);
        $estBySym[$symRow] = $row;
      }
    }
  }
} catch (\Throwable $e) {
  $forwardAvailable = false;
}

// ---------- 5) Rows (Basis) ----------
$rows = [];
foreach ($symbols as $s) {
  $q = $quotes[$s] ?? null; if (!$q) continue;
  $price = $q['price'] ?? null;

  $pe_ttm  = $q['pe'] ?? ($q['peRatio'] ?? null);
  $eps_ttm = $q['eps'] ?? ($q['epsTtm'] ?? null);
  if (($eps_ttm===null || $eps_ttm==0) && $price!==null && $pe_ttm!==null && (float)$pe_ttm!=0.0) {
    $eps_ttm = (float)$price/(float)$pe_ttm;
  }

  $est = $estBySym[$s] ?? null;
  $eps_fy1 = null;
  if ($forwardAvailable && is_array($est)) {
    $eps_fy1 = pick_first($est, [
      'epsEstimatedAvg','epsAvg','estimatedEPSAvg','epsEstimateAvg','epsEstimated'
    ]);
    if (!is_numeric($eps_fy1)) $eps_fy1 = null;
  }

  $pe_fwd = (is_numeric($price) && is_numeric($eps_fy1) && (float)$eps_fy1!=0.0) ? (float)$price/(float)$eps_fy1 : null;
  $eps_growth_fy1 = (is_numeric($eps_fy1) && is_numeric($eps_ttm) && (float)$eps_ttm!=0.0)
    ? (($eps_fy1 - $eps_ttm) / abs($eps_ttm))
    : null;

  $mcapRaw = (isset($q['marketCap']) && is_numeric($q['marketCap'])) ? (float)$q['marketCap'] : null;

  $rows[] = [
    'symbol'       => $s,
    'name'         => $meta[$s]['name'] ?? ($q['name'] ?? null),
    'industry'     => $meta[$s]['industry'] ?? null,
    'country'      => $meta[$s]['country'] ?? null,
    'exchange'     => $meta[$s]['exchange'] ?? ($q['exchange'] ?? null),
    'price'        => is_numeric($price) ? (float)$price : null,
    'pe'           => is_numeric($pe_ttm) ? (float)$pe_ttm : null,
    'eps'          => is_numeric($eps_ttm)? (float)$eps_ttm: null,
    'pe_fwd'       => is_numeric($pe_fwd) ? (float)$pe_fwd : null,
    'eps_fy1'      => is_numeric($eps_fy1)? (float)$eps_fy1: null,
    'eps_growth'   => is_numeric($eps_growth_fy1)? (float)$eps_growth_fy1: null,
    'marketCap'    => $mcapRaw,
    'marketCapFmt' => format_marketcap($mcapRaw),

    // Qualität (später befüllt)
    'roic'         => null,
    'grossMargin'  => null,
    'fcfMargin'    => null,

    // Scores
    'score'        => null,
    'scoreGrowth'  => null,
    'scoreValue'   => null,
    'scoreQuality' => null,

    'isHit'        => false,
  ];
}
if (!$rows) {
  echo json_encode([
    'mode'    => $forwardAvailable ? 'forward' : 'ttm',
    'sector'  => $sector,
    'filters' => ['industry'=>$industry?:null,'country'=>$country?:null,'exchange'=>$exchange?:null],
    'counts'  => ['screener'=>$initialCount,'after_local'=>$afterLocal,'quotes'=>$quotesCount,'filtered'=>0],
    'averages'=> $forwardAvailable ? ['pe_fwd'=>null,'eps_growth'=>null] : ['pe'=>null,'eps'=>null],
    'items'   => [],
    'items_all'=>[]
  ], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT); exit;
}

// Forward-Coverage prüfen
$cov_forward_count = 0;
foreach ($rows as $r) {
  if (is_numeric($r['pe_fwd']) && is_numeric($r['eps_growth'])) $cov_forward_count++;
}
$cov_forward_ratio = $afterLocal ? $cov_forward_count / $afterLocal : 0.0;
if ($forwardAvailable && $cov_forward_ratio < $coverageMin) {
  $forwardAvailable = false;
}

// ---------- 6) Basics-Filter (mit Soft-Fallback) ----------
$rows_basic = array_values(array_filter($rows, function($r) use($minPrice,$minMcap,$forwardAvailable){
  if (!is_numeric($r['price']) || $r['price'] < $minPrice) return false;
  if (!is_numeric($r['marketCap']) || $r['marketCap'] < $minMcap) return false;
  if ($forwardAvailable) {
    return (is_numeric($r['eps_fy1']) && $r['eps_fy1'] > 0);
  } else {
    return (is_numeric($r['eps']) && $r['eps'] > 0);
  }
}));
if (count($rows_basic) === 0) {
  $looserPrice = max(1.0, $minPrice / 2);
  $looserMcap  = max(100_000_000, $minMcap / 4);
  $rows_basic = array_values(array_filter($rows, function($r) use($looserPrice,$looserMcap,$forwardAvailable){
    if (!is_numeric($r['price']) || $r['price'] < $looserPrice) return false;
    if (!is_numeric($r['marketCap']) || $r['marketCap'] < $looserMcap) return false;
    if ($forwardAvailable) {
      return (is_numeric($r['eps_fy1']) && $r['eps_fy1'] > 0);
    } else {
      return (is_numeric($r['eps']) && $r['eps'] > 0);
    }
  }));
}

// ---------- 7) Vor-Score (Growth vs Value) für Kandidaten (Quality-Fetch) ----------
if ($forwardAvailable) {
  $arr_pe = array_column($rows_basic, 'pe_fwd');
  $arr_gr = array_column($rows_basic, 'eps_growth');
  foreach ($rows_basic as &$r) {
    $rp = clamp01(is_numeric($r['pe_fwd'])    ? 1.0 - rank_percentile_one((float)$r['pe_fwd'], $arr_pe) : null);
    $rg = clamp01(is_numeric($r['eps_growth'])?       rank_percentile_one((float)$r['eps_growth'], $arr_gr) : null);
    $r['_preScore'] = ($rp!==null && $rg!==null) ? ($rg - $rp) : null;
  } unset($r);
} else {
  $arr_pe = array_column($rows_basic, 'pe');
  $arr_e  = array_column($rows_basic, 'eps');
  foreach ($rows_basic as &$r) {
    $rp = clamp01(is_numeric($r['pe']) ? 1.0 - rank_percentile_one((float)$r['pe'], $arr_pe) : null);
    $re = clamp01(is_numeric($r['eps'])?       rank_percentile_one((float)$r['eps'], $arr_e)  : null);
    $r['_preScore'] = ($rp!==null && $re!==null) ? ($re - $rp) : null;
  } unset($r);
}
$rows_scored_pre = array_values(array_filter($rows_basic, fn($r)=> $r['_preScore']!==null));
usort($rows_scored_pre, function($a,$b){ return ($b['_preScore'] <=> $a['_preScore']); });
$candidates   = array_slice($rows_scored_pre, 0, min($qualityMax, count($rows_scored_pre)));
$candSymbols  = array_column($candidates, 'symbol');

// ---------- 8) Qualität (ratios_ttm) nur für Kandidaten ----------
$qual = [];
if ($candSymbols) {
  $reqR = [];
  foreach ($candSymbols as $idx=>$sym) {
    $reqR["r$idx"] = ['key'=>'ratios_ttm','vars'=>['symbol'=>$sym]];
    if (count($reqR) === 80) {
      $bat = fmp_multi($reqR, 20);
      foreach ($bat as $rowsR) {
        $row = first_row($rowsR);
        if (!$row) continue;
        $symR = $row['symbol'] ?? null; if (!$symR) continue;
        $qual[$symR] = [
          'roic' => $row['returnOnInvestedCapitalTTM'] ?? ($row['returnOnEquityTTM'] ?? null),
          'gm'   => $row['grossProfitMarginTTM'] ?? null,
          'fcfM' => $row['freeCashFlowMarginTTM'] ?? null,
        ];
      }
      $reqR = [];
    }
  }
  if ($reqR) {
    $bat = fmp_multi($reqR, 20);
    foreach ($bat as $rowsR) {
      $row = first_row($rowsR);
      if (!$row) continue;
      $symR = $row['symbol'] ?? null; if (!$symR) continue;
      $qual[$symR] = [
        'roic' => $row['returnOnInvestedCapitalTTM'] ?? ($row['returnOnEquityTTM'] ?? null),
        'gm'   => $row['grossProfitMarginTTM'] ?? null,
        'fcfM' => $row['freeCashFlowMarginTTM'] ?? null,
      ];
    }
  }
  // Mergen
  $bySymIndex = [];
  foreach ($rows_basic as $i=>$r) $bySymIndex[$r['symbol']] = $i;
  foreach ($qual as $sym=>$qv) {
    if (!isset($bySymIndex[$sym])) continue;
    $i = $bySymIndex[$sym];
    $rows_basic[$i]['roic']        = is_numeric($qv['roic']) ? (float)$qv['roic'] : null;
    $rows_basic[$i]['grossMargin'] = is_numeric($qv['gm'])   ? (float)$qv['gm']   : null;
    $rows_basic[$i]['fcfMargin']   = is_numeric($qv['fcfM']) ? (float)$qv['fcfM'] : null;
  }
}

// --- Soft-Qualitäts-Penalty anhand Min. ROIC ---
// Falls ROIC unter Schwelle: kleiner Abzug vom Endscore (z. B. 0.08)
foreach ($rows_basic as &$r) {
  $r['_q_penalty'] = 0.0;
  if (is_numeric($r['roic']) && $r['roic'] < $minRoic) {
    $r['_q_penalty'] = 0.08; // fein dosieren, z.B. 0.05–0.15
  }
}
unset($r);

// ---------- 9) P/E & EPS Rekonstruktion + sanftes Sanitizing ----------
foreach ($rows_basic as &$r) {
  $price = (is_numeric($r['price']) ? (float)$r['price'] : null);

  // --- TTM: EPS <-> P/E gegenseitig rekonstruieren ---
  $eps = (is_numeric($r['eps']) ? (float)$r['eps'] : null);
  $pe  = (is_numeric($r['pe'])  ? (float)$r['pe']  : null);

  if ($eps === null && $price !== null && $pe !== null && $pe != 0.0) {
    $eps = $price / $pe;
  }
  if (($pe === null || $pe <= 0) && $price !== null && $eps !== null && $eps != 0.0) {
    $pe = $price / $eps;
  }

  // --- Forward: EPS_FY1 <-> P/E_FWD gegenseitig rekonstruieren ---
  $eps1 = (is_numeric($r['eps_fy1']) ? (float)$r['eps_fy1'] : null);
  $pef  = (is_numeric($r['pe_fwd'])  ? (float)$r['pe_fwd']  : null);

  if ($eps1 === null && $price !== null && $pef !== null && $pef != 0.0) {
    $eps1 = $price / $pef;
  }
  if (($pef === null || $pef <= 0) && $price !== null && $eps1 !== null && $eps1 != 0.0) {
    $pef = $price / $eps1;
  }

  // --- Validität: negative/Null-P/E rauswerfen ---
  if ($pe  !== null && $pe  <= 0) $pe  = null;
  if ($pef !== null && $pef <= 0) $pef = null;

  // --- Artefakt-Guard: sehr niedrige P/E bei positivem EPS abfangen ---
  if ($peFloor > 0) {
    if ($pe  !== null && $eps  !== null && $eps  > 0 && $pe  < $peFloor) $pe  = null;
    if ($pef !== null && $eps1 !== null && $eps1 > 0 && $pef < $peFloor) $pef = null;
  }

  // zurückschreiben
  $r['eps']      = $eps;
  $r['pe']       = $pe;
  $r['eps_fy1']  = $eps1;
  $r['pe_fwd']   = $pef;
}
unset($r);

// ---------- 10) Finale Score-Berechnung (Gruppen-Ranking mit Sektor-Fallback) ----------
$keyGroup = 'industry';
if ($forwardAvailable) {
  $arrP  = array_column($rows_basic, 'pe_fwd');
  $arrG  = array_column($rows_basic, 'eps_growth');
  $arrR  = array_column($rows_basic, 'roic');
  $arrGM = array_column($rows_basic, 'grossMargin');
  $arrFCF= array_column($rows_basic, 'fcfMargin');

  $G_pe   = group_arrays_by($rows_basic, $keyGroup, 'pe_fwd');
  $G_gr   = group_arrays_by($rows_basic, $keyGroup, 'eps_growth');
  $G_roic = group_arrays_by($rows_basic, $keyGroup, 'roic');
  $G_gm   = group_arrays_by($rows_basic, $keyGroup, 'grossMargin');
  $G_fcf  = group_arrays_by($rows_basic, $keyGroup, 'fcfMargin');

  foreach ($rows_basic as &$r) {
    $gKey = $r[$keyGroup] ?? 'UNKNOWN';
    $v  = clamp01(is_numeric($r['pe_fwd'])     ? (1.0 - rank_in_group_or_sector((float)$r['pe_fwd'],     $G_pe[$gKey]??[],  $arrP,  $MIN_GROUP)) : null);
    $g  = clamp01(is_numeric($r['eps_growth']) ?        rank_in_group_or_sector((float)$r['eps_growth'], $G_gr[$gKey]??[],  $arrG,  $MIN_GROUP) : null);
    $q1 = clamp01(is_numeric($r['roic'])       ?        rank_in_group_or_sector((float)$r['roic'],       $G_roic[$gKey]??[],$arrR, $MIN_GROUP) : null);
    $q2 = clamp01(is_numeric($r['grossMargin'])?        rank_in_group_or_sector((float)$r['grossMargin'],$G_gm[$gKey]??[],  $arrGM,$MIN_GROUP) : null);
    $q3 = clamp01(is_numeric($r['fcfMargin'])  ?        rank_in_group_or_sector((float)$r['fcfMargin'],  $G_fcf[$gKey]??[], $arrFCF,$MIN_GROUP) : null);

    $qParts = array_values(array_filter([$q1,$q2,$q3], fn($x)=> $x!==null));
    $Q = $qParts ? (array_sum($qParts)/count($qParts)) : null;

    $r['scoreGrowth']  = $g;
    $r['scoreValue']   = $v;
    $r['scoreQuality'] = $Q;
    $r['score'] = ($g!==null && $v!==null) ? max(0.0, (0.5*$g + 0.3*($Q ?? 0.0) + 0.2*$v) - ($r['_q_penalty'] ?? 0.0)) : null;
  } unset($r);

  $averages = [
    'pe_fwd'     => $useMedian ? median($arrP) : mean($arrP),
    'eps_growth' => $useMedian ? median($arrG) : mean($arrG),
    'agg'        => $useMedian ? 'median' : 'mean'
  ];
} else {
  $arrP  = array_column($rows_basic, 'pe');
  $arrE  = array_column($rows_basic, 'eps');
  $arrR  = array_column($rows_basic, 'roic');
  $arrGM = array_column($rows_basic, 'grossMargin');
  $arrFCF= array_column($rows_basic, 'fcfMargin');

  $G_pe   = group_arrays_by($rows_basic, $keyGroup, 'pe');
  $G_eps  = group_arrays_by($rows_basic, $keyGroup, 'eps');
  $G_roic = group_arrays_by($rows_basic, $keyGroup, 'roic');
  $G_gm   = group_arrays_by($rows_basic, $keyGroup, 'grossMargin');
  $G_fcf  = group_arrays_by($rows_basic, $keyGroup, 'fcfMargin');

  foreach ($rows_basic as &$r) {
    $gKey = $r[$keyGroup] ?? 'UNKNOWN';
    $v  = clamp01(is_numeric($r['pe'])  ? (1.0 - rank_in_group_or_sector((float)$r['pe'],  $G_pe[$gKey]??[],  $arrP,  $MIN_GROUP)) : null);
    $g  = clamp01(is_numeric($r['eps']) ?        rank_in_group_or_sector((float)$r['eps'], $G_eps[$gKey]??[], $arrE,  $MIN_GROUP) : null);
    $q1 = clamp01(is_numeric($r['roic'])       ? rank_in_group_or_sector((float)$r['roic'],       $G_roic[$gKey]??[],$arrR, $MIN_GROUP) : null);
    $q2 = clamp01(is_numeric($r['grossMargin'])? rank_in_group_or_sector((float)$r['grossMargin'],$G_gm[$gKey]??[],  $arrGM,$MIN_GROUP) : null);
    $q3 = clamp01(is_numeric($r['fcfMargin'])  ? rank_in_group_or_sector((float)$r['fcfMargin'],  $G_fcf[$gKey]??[], $arrFCF,$MIN_GROUP) : null);

    $qParts = array_values(array_filter([$q1,$q2,$q3], fn($x)=> $x!==null));
    $Q = $qParts ? (array_sum($qParts)/count($qParts)) : null;

    $r['scoreGrowth']  = $g;
    $r['scoreValue']   = $v;
    $r['scoreQuality'] = $Q;
    $r['score'] = ($g!==null && $v!==null) ? max(0.0, (0.5*$g + 0.3*($Q ?? 0.0) + 0.2*$v) - ($r['_q_penalty'] ?? 0.0)) : null;
  } unset($r);

  $averages = [
    'pe'   => $useMedian ? median($arrP) : mean($arrP),
    'eps'  => $useMedian ? median($arrE) : mean($arrE),
    'agg'  => $useMedian ? 'median' : 'mean'
  ];
}

// ---------- 11) Sortieren & „Never-Zero“-Rettungsringe ----------
$rows_scored = array_values(array_filter($rows_basic, fn($r)=> $r['score']!==null));

// Fallback A: Wenn keine Scores -> baue Not-Score (Growth + Value via EY)
if (count($rows_scored) === 0) {
  if ($forwardAvailable) {
    $arrG = array_column($rows_basic, 'eps_growth');
    $arrPE= array_column($rows_basic, 'pe_fwd');
    $arrEY= [];
    foreach ($rows_basic as $rb) {
      $arrEY[] = (is_numeric($rb['eps_fy1']) && is_numeric($rb['price']) && $rb['price']>0) ? ($rb['eps_fy1']/$rb['price']) : null;
    }
    foreach ($rows_basic as &$r) {
      $g  = clamp01(is_numeric($r['eps_growth']) ? rank_percentile_one((float)$r['eps_growth'], $arrG) : null);
      $vPE= clamp01(is_numeric($r['pe_fwd'])     ? (1.0 - rank_percentile_one((float)$r['pe_fwd'], $arrPE)) : null);
      $ey = (is_numeric($r['eps_fy1']) && is_numeric($r['price']) && $r['price']>0) ? ($r['eps_fy1']/$r['price']) : null;
      $vEY= clamp01(is_numeric($ey) ? rank_percentile_one((float)$ey, $arrEY) : null);
      $v  = ($vPE!==null) ? $vPE : $vEY;
      $r['score'] = ($g!==null && $v!==null) ? (0.6*$g + 0.4*$v) : null;
    }
    unset($r);
  } else {
    $arrE = array_column($rows_basic, 'eps');
    $arrPE= array_column($rows_basic, 'pe');
    $arrEY= [];
    foreach ($rows_basic as $rb) {
      $arrEY[] = (is_numeric($rb['eps']) && is_numeric($rb['price']) && $rb['price']>0) ? ($rb['eps']/$rb['price']) : null;
    }
    foreach ($rows_basic as &$r) {
      $g  = clamp01(is_numeric($r['eps']) ? rank_percentile_one((float)$r['eps'], $arrE) : null);
      $vPE= clamp01(is_numeric($r['pe'])  ? (1.0 - rank_percentile_one((float)$r['pe'], $arrPE)) : null);
      $ey = (is_numeric($r['eps']) && is_numeric($r['price']) && $r['price']>0) ? ($r['eps']/$r['price']) : null;
      $vEY= clamp01(is_numeric($ey) ? rank_percentile_one((float)$ey, $arrEY) : null);
      $v  = ($vPE!==null) ? $vPE : $vEY;
      $r['score'] = ($g!==null && $v!==null) ? (0.6*$g + 0.4*$v) : null;
    }
    unset($r);
  }
  $rows_scored = array_values(array_filter($rows_basic, fn($r)=> $r['score']!==null));
}

// Fallback B: Wenn immer noch leer -> PreScore verwenden
if (count($rows_scored) === 0) {
  $rows_ps = array_values(array_filter($rows_basic, fn($r)=> isset($r['_preScore']) && $r['_preScore']!==null));
  if (!empty($rows_ps)) {
    usort($rows_ps, fn($a,$b)=> ($b['_preScore'] <=> $a['_preScore']));
    foreach ($rows_ps as &$r) { if (!isset($r['score']) || $r['score']===null) $r['score'] = $r['_preScore']; }
    unset($r);
    $rows_scored = $rows_ps;
  }
}

// Sortieren & Top-K markieren
if (!empty($rows_scored)) {
  usort($rows_scored, function($a,$b){
    $sa = $a['score'] ?? -INF; $sb = $b['score'] ?? -INF;
    if ($sa == $sb) {
      $qa = ($a['scoreQuality'] ?? null); $qb = ($b['scoreQuality'] ?? null);
      if ($qa !== $qb) return ($qb <=> $qa);
      $pa = ($a['pe_fwd'] ?? $a['pe'] ?? INF);
      $pb = ($b['pe_fwd'] ?? $b['pe'] ?? INF);
      return $pa <=> $pb;
    }
    return $sb <=> $sa;
  });
}
$hits = 0;
for ($i=0; $i<min($topK, count($rows_scored)); $i++) { $rows_scored[$i]['isHit'] = true; $hits++; }

// ---------- 12) Output ----------
echo json_encode([
  'mode'      => $forwardAvailable ? 'forward' : 'ttm',
  'sector'    => $sector,
  'filters'   => ['industry'=>$industry?:null,'country'=>$country?:null,'exchange'=>$exchange?:null],
  'counts'    => ['screener'=>$initialCount,'after_local'=>$afterLocal,'quotes'=>$quotesCount,'filtered'=>$hits],
  'averages'  => $averages,
  'ranking'   => [
    'formula' => $forwardAvailable
      ? '0.5*rank(growth FY1E) + 0.3*rank(quality) + 0.2*(1-rank(fwd PE)) (grouped by sub-industry; sector fallback)'
      : '0.5*rank(EPS TTM) + 0.3*rank(quality) + 0.2*(1-rank(PE TTM)) (grouped by sub-industry; sector fallback)',
    'top_k'   => $topK
  ],
  'thresholds'=> [
    'pe_floor'     => $peFloor,
    'min_roic'     => $minRoic,
    'sector_defaults' => $sectorCfg, // die verwendeten Defaults dieses Sektors
  ],
  'coverage'  => ['forward_ratio'=>$cov_forward_ratio,'coverage_min'=>$coverageMin],
  'items'     => array_slice($rows_scored, 0, $topK),
  'items_all' => $rows_scored
], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);