// Which visualizations does the generator actually reach, and why not the rest?
//
// A library entry that no song ever sees is not a library entry. But "never cast
// on one track across many seeds" and "never cast at all" are completely
// different findings, and the first is what the variety report measures, so this
// varies the SONG as well as the seed and separates the two.
//
// The casting funnel has four gates, and a scene can die at any of them:
//
//   1. role       accents are never a section's primary scene, by design.
//   2. signature  the track picks one or two traits and a scene that cannot
//                 honour both is disqualified for that whole track.
//   3. director   the track's point of view maps each section KIND to three
//                 families; a scene in none of them is not a candidate.
//   4. roster     a kind takes only three or four scenes, weighted.
//
// Reporting a scene as "never cast" without saying which gate killed it is
// unactionable — gate 2 is a trait declaration to fix on the scene, gate 3 is a
// director table to widen, gate 4 is arithmetic that no amount of either fixes.
//
//   node tools/cast-census.js            12 songs x 40 seeds
//   node tools/cast-census.js 24 80      more of both

import { FeatureTrack } from '../src/audio/FeatureTrack.js';
import { synthesizeArrangement } from '../src/audio/synth.js';
import { generateLook } from '../src/look/LookGenerator.js';
import { scenes } from '../src/scenes/registry.js';
import { sceneHonours, TRAITS } from '../src/look/Personality.js';
import { DIRECTORS } from '../src/look/directors.js';

const SONGS = Number(process.argv[2]) || 12;
const SEEDS = Number(process.argv[3]) || 40;

/**
 * A spread of songs, not a spread of seeds.
 *
 * Tempo, density and brightness are the three things the generator actually
 * reads, so the bank spans them deliberately rather than randomly — a random
 * bank would cluster in the middle and under-report exactly the scenes that only
 * a fast, bright, noisy track can reach.
 *
 * Arrangements, not `synthesizeSectioned`. A two-section track is all quiet
 * kinds, and quiet kinds are restricted to the restful families for every
 * director, so half the library would be unreachable for reasons that have
 * nothing to do with casting.
 */
function songBank(count) {
    const out = [];
    for (let i = 0; i < count; i++) {
        const t = count === 1 ? 0.5 : i / (count - 1);
        const bpm = 70 + t * 104;
        out.push({
            label: `bpm ${Math.round(bpm)}`,
            track: FeatureTrack.fromAudioBuffer(synthesizeArrangement({
                bpm,
                duration: 120,
                brightness: 0.6 + ((i * 7) % 5) / 5 * 1.2,
                density: 0.55 + ((i * 3) % 4) / 4 * 0.8,
            }), { fps: 60 }),
        });
    }
    return out;
}

const pool = scenes.filter((m) => m.role !== 'accent');
const stat = new Map(pool.map((m) => [m.name, {
    module: m, eligible: 0, candidate: 0, cast: 0, background: 0, songs: new Set(),
}]));
const kindTally = new Map();

const songs = songBank(SONGS);
let looks = 0;

for (const { track } of songs) {
    for (let s = 0; s < SEEDS; s++) {
        const seed = (0x51ee7 + s * 2654435761) >>> 0;
        const look = generateLook(track, { seed });
        looks++;

        const signature = look.personality.signature;
        const director = DIRECTORS.find((d) => d.name === look.director);
        const kinds = [...new Set(look.sections.map((x) => x.kind))];
        for (const k of kinds) kindTally.set(k, (kindTally.get(k) || 0) + 1);
        const families = new Set(kinds.flatMap((k) => director.families[k] || []));

        for (const m of pool) {
            const st = stat.get(m.name);
            const honours = sceneHonours(m, signature);
            if (honours) st.eligible++;
            // A candidate is a scene that survived BOTH the signature and the
            // director for at least one kind this track actually has.
            if (honours && families.has(m.family)) st.candidate++;
        }

        // Background vs any-layer, kept apart. A scene that only ever appears as
        // a translucent overlay is not "in the video" in the sense that matters:
        // it is texture over someone else's shot, and counting it as a cast is
        // how a library can look fully used while the same nine scenes carry
        // every frame. Overlays are also drawn from the whole signature-eligible
        // library rather than the director's families, so counting them together
        // hides which gate is doing the work.
        const usedHere = new Set(look.sections.flatMap(
            (sec) => sec.variants.flatMap((v) => v.map((l) => l.module.name))));
        const backgroundsHere = new Set(look.sections.flatMap(
            (sec) => sec.variants.map((v) => v[0].module.name)));
        for (const name of usedHere) {
            const st = stat.get(name);
            if (!st) continue;                       // an accent layer
            st.cast++;
            st.songs.add(track);
        }
        for (const name of backgroundsHere) {
            const st = stat.get(name);
            if (st) st.background++;
        }
    }
}

const rows = [...stat.values()].map((st) => ({
    name: st.module.name,
    family: st.module.family,
    traits: (st.module.traits || []).map((t) => t[0]).join(''),
    eligible: st.eligible / looks,
    candidate: st.candidate / looks,
    cast: st.cast / looks,
    background: st.background / looks,
    songs: st.songs.size,
    // The conditional is the whole point: a scene that is a candidate often and
    // cast never is losing the roster draw, which is a different bug from a
    // scene that is never a candidate at all.
    given: st.candidate ? st.cast / st.candidate : 0,
})).sort((a, b) => a.background - b.background || a.cast - b.cast);

const pc = (x) => `${(x * 100).toFixed(1)}%`.padStart(6);

console.log(`\n${looks} looks · ${SONGS} songs x ${SEEDS} seeds · ${pool.length} castable scenes\n`);
console.log('  scene                    family      traits  eligible candidate  as bg   any layer  songs');
console.log('  ' + '-'.repeat(96));
for (const r of rows) {
    const flag = r.background === 0 && r.cast === 0 ? '  ← NEVER'
        : r.background === 0 ? '  ← overlay only'
        : r.background < 0.02 ? '  ← rare' : '';
    console.log(`  ${r.name.padEnd(24)} ${r.family.padEnd(11)} ${r.traits.padEnd(6)} ` +
        `${pc(r.eligible)} ${pc(r.candidate)} ${pc(r.background)} ${pc(r.cast)}  ` +
        `${String(r.songs).padStart(3)}/${SONGS}${flag}`);
}

console.log(`\n  section kinds the bank produced: ` +
    [...kindTally.entries()].sort((a, b) => b[1] - a[1])
        .map(([k, v]) => `${k} ${(v / looks * 100).toFixed(0)}%`).join(' · '));

// --- where the losses happen ---------------------------------------------
const never = rows.filter((r) => r.background === 0);
const neverCandidate = never.filter((r) => r.candidate === 0);
const neverEligible = never.filter((r) => r.eligible === 0);
const lostInRoster = never.filter((r) => r.candidate > 0);

console.log(`\n  never cast as a background: ${never.length}/${pool.length}` +
    `  (of those, ${never.filter((r) => r.cast > 0).length} appear only as overlays)`);
console.log(`    disqualified by the signature gate, always:  ${neverEligible.length}` +
    (neverEligible.length ? `  (${neverEligible.map((r) => r.name).join(', ')})` : ''));
console.log(`    eligible but never a candidate (director):   ${neverCandidate.length - neverEligible.length}` +
    (neverCandidate.length - neverEligible.length
        ? `  (${neverCandidate.filter((r) => r.eligible > 0).map((r) => r.name).join(', ')})` : ''));
console.log(`    a candidate but never won a roster slot:     ${lostInRoster.length}` +
    (lostInRoster.length ? `  (${lostInRoster.map((r) => r.name).join(', ')})` : ''));

// --- the trait declarations behind gate 2 ---------------------------------
console.log('\n  trait coverage across the library (a signature is one or two of these):');
for (const trait of TRAITS) {
    const n = pool.filter((m) => (m.traits || []).includes(trait)).length;
    console.log(`    ${trait.padEnd(7)} ${String(n).padStart(3)}/${pool.length} scenes declare it`);
}
const byCount = new Map();
for (const m of pool) {
    const k = (m.traits || []).length;
    byCount.set(k, (byCount.get(k) || 0) + 1);
}
console.log('\n  scenes by number of traits declared (fewer traits = harder to cast):');
for (const k of [...byCount.keys()].sort()) {
    console.log(`    ${k} traits  ${String(byCount.get(k)).padStart(3)} scenes` +
        (k <= 1 ? '   ← can only survive a signature it happens to match' : ''));
}

// --- family reachability per director -------------------------------------
console.log('\n  family demand — how many of the 6 section kinds each director opens to a family:');
const families = [...new Set(pool.map((m) => m.family))].sort();
const head = families.map((f) => f.slice(0, 6).padStart(7)).join('');
console.log(`    ${'director'.padEnd(12)}${head}`);
for (const d of DIRECTORS) {
    const counts = families.map((f) => {
        const n = Object.values(d.families).filter((list) => list.includes(f)).length;
        return String(n).padStart(7);
    }).join('');
    console.log(`    ${d.name.padEnd(12)}${counts}`);
}
const sizes = families.map((f) => `${f} ${pool.filter((m) => m.family === f).length}`);
console.log(`    library holds: ${sizes.join(' · ')}`);
