import './style.css';
import { createLife, hatch, touchEgg, stage, expression, receiveEntry, isHibernating, wake, WEEK, THOUGHT_INTERVAL, thoughtWait, seedAvailable, feedSeed, advanceNight, isNightSleeping } from './life.ts';
import { worldTime, type Realm } from './world.ts';
import { PresenceAudio } from './audio.ts';
import { openNotebook, type Entry } from './notebook.ts';
import { ReflectionService, generateReflection, type Reflection } from './reflection.ts';

const app = document.querySelector<HTMLDivElement>('#app')!;
app.innerHTML = `
  <main class="world" data-realm="home">
    <div class="night-wash" aria-hidden="true"></div>
    <div class="orb" aria-hidden="true"></div>
    <div class="hill hill-far" aria-hidden="true"></div><div class="hill hill-near" aria-hidden="true"></div>
    <header><a class="wordmark" href="./" aria-label="OBU home">OBU<span>a little room for wonder</span></a><button id="sound" class="quiet" aria-pressed="false">Sound off</button></header>
    <div class="scene-heading"><span class="eyebrow" id="place">SOMEWHERE, SOFTLY</span><h1>Let the world be a little quieter.</h1><p>Nothing to solve. A little space to be.</p></div>
    <section class="encounter" aria-label="OBU's environment">
      <button id="creature" class="creature" aria-label="Touch the egg to hatch OBU"><img src="/assets/egg.svg" alt="" draggable="false" /></button>
      <nav id="realm-stones" class="realm-stones" aria-label="Realm navigation" hidden><button id="home-stone" class="realm-token" aria-label="Home realm"><span aria-hidden="true">⌂</span>Home</button><button id="dream-stone" class="realm-token" aria-label="Dream realm"><span aria-hidden="true">☾</span>Dream</button></nav>
      <div class="ground-shadow" aria-hidden="true"></div>
      <p id="hint">Something is waiting. Touch the egg.</p>
      <button id="awaken" class="quiet" hidden>Awaken OBU</button>
      <p id="growth-note" class="growth-note"></p>
      <p id="realm-note" class="growth-note" hidden>Dream opens on OBU’s first night as an adult.</p>
      <p id="utterance" role="status" aria-live="polite"></p>
      <p id="response-status" class="response-status" role="status"></p>
      <p id="safety-support" class="safety-support" role="alert" hidden></p>
      <button id="few-words" class="quiet" hidden>A few words?</button>
    </section>
    <div id="seed-tray" class="seed-tray" hidden><p id="seed-hint">Drag the growth seed to OBU to help OBU grow.</p><button id="growth-seed" class="growth-seed" aria-label="Select growth seed" aria-pressed="false">❧</button><button id="give-seed" class="quiet" hidden>Give to OBU</button></div>
    <nav class="actions" aria-label="Quiet activities"><button id="thought" class="primary" disabled>+ &nbsp; Leave a thought</button><button id="dream" class="quiet" hidden>Enter Dream ↗</button><button id="notebook" class="quiet">Open notebook <span id="count"></span></button></nav>
    <p id="storage-error" role="alert" class="note"></p><footer><span id="time-label"></span><span>SAVED IN THIS BROWSER</span></footer>
  </main>
  <dialog id="entry-dialog"><form id="entry-form"><div class="dialog-top"><span class="eyebrow">A LITTLE SPACE</span><button type="button" data-close aria-label="Close entry">×</button></div><h2>What is passing through?</h2><label for="kind">Entry type</label><select id="kind"><option value="thought">Thought</option><option value="dream">Dream</option></select><label for="entry-text">Your words</label><textarea id="entry-text" rows="6" maxlength="10000" placeholder="A thought, an image, a small uncertainty…" required></textarea><p class="note">Entries are saved in this browser across reloads. They are not synced or backed up. Clearing site data removes them. “Give to OBU” sends only this entry to your local AI server for a possible reflection. “Just keep this” skips AI. Reflections are saved with the entry.</p><p id="save-error" role="alert" class="note"></p><div class="submit-actions"><button id="give-thought" class="primary" type="submit" name="mode" value="reflect">Give to OBU</button><button class="quiet" type="submit" name="mode" value="keep">Just keep this</button></div></form></dialog>
  <dialog id="library-dialog"><div class="dialog-top"><span class="eyebrow">THE NOTEBOOK</span><button data-close aria-label="Close notebook">×</button></div><h2>Things that passed through.</h2><p class="note">Saved in this browser. Anyone using this browser profile can open this notebook. Clearing site data removes it.</p><button id="record-dream" class="quiet">+ Record a dream</button><div id="entries"></div></dialog>
  ${import.meta.env.DEV ? `<details class="dev"><summary>Scene preview</summary><label>Light <select id="preview-time"><option value="live">Local time</option><option value="12">Day</option><option value="23">Night</option></select></label><label>Life <select id="preview-stage"><option value="egg">Egg</option><option value="baby">Early Baby</option><option value="fragment">Maturing Baby</option><option value="adult">Adult</option></select></label><button id="preview-sleep">Preview hibernation</button><button id="preview-voice">Voice sample</button><p>Preview changes are temporary. Reload to return to your saved creature. Saving is disabled during preview. Voice samples are authored examples, never interpretations.</p></details>` : ''}`;

const get = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
const world = document.querySelector<HTMLElement>('.world')!;
const creature = get<HTMLButtonElement>('creature');
const entryDialog = get<HTMLDialogElement>('entry-dialog');
const libraryDialog = get<HTMLDialogElement>('library-dialog');
const audio = new PresenceAudio();
let notebook: ReturnType<typeof openNotebook> | undefined;
try { notebook = openNotebook(localStorage, crypto.randomUUID()); }
catch { get('storage-error').textContent = 'The saved notebook could not be opened. Existing data has not been changed. Check browser storage access, then reload.'; }
let life = notebook?.initial.life ?? createLife(crypto.randomUUID());
let previewing = false;
let realm: Realm = 'home';
let reactionTimer: ReturnType<typeof setTimeout>;
let timeOverride: number | null = null;
let entries: Entry[] = notebook?.initial.entries ?? [];
let activeEntryId: string | null = null;
let selectedSeed = false;
let hatchLocked = false;
const zone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
function updateLife(next: typeof life) { if (previewing) { life = next; return true; } return persist(next); }
const responseBusy = new Set<string>();
const responseErrors = new Map<string, string>();
const reflections = new ReflectionService(generateReflection, id => entries.find(e => e.id === id), entry =>
  persist(life, entries.map(e => e.id === entry.id ? entry : e)));
function showReflection(result: Reflection) {
  get('utterance').textContent = result.utterance ?? '';
  get('safety-support').textContent = result.support ?? '';
  get('safety-support').hidden = !result.support;
  react(result.utterance ? 'speaking' : result.reaction);
  if (result.reaction === 'purr') void audio.play();
}
async function requestWords(id: string, explicit: boolean) {
  if (previewing || responseBusy.has(id)) return;
  responseBusy.add(id); responseErrors.delete(id);
  if (activeEntryId === id) {
    get('response-status').textContent = 'Saved. OBU is receiving your words…';
    get('utterance').textContent = '';
    get('safety-support').hidden = true;
  }
  render(); renderLibrary();
  try {
    const response = await reflections.request(id, explicit);
    if (response && activeEntryId === id && !previewing) {
      showReflection(response);
      get('response-status').textContent = response.utterance || response.support ? 'Reflection saved with this entry.' : 'OBU rests with this thought. A few words are available if you want them.';
    }
  } catch (error) {
    const message = error instanceof Error && error.name !== 'TimeoutError' ? error.message : 'Your entry is saved. The local model took too long. Try again.';
    responseErrors.set(id, message);
    if (activeEntryId === id) get('response-status').textContent = message;
  } finally {
    responseBusy.delete(id); render(); renderLibrary();
  }
}
get('few-words').addEventListener('click', () => { if (activeEntryId) void requestWords(activeEntryId, true); });
function persist(nextLife = life, nextEntries = entries) {
  if (previewing) return false;
  try {
    if (!notebook) throw new Error('Storage unavailable');
    notebook.save({ version: 1, life: nextLife, entries: nextEntries });
    life = nextLife; entries = nextEntries;
    get('storage-error').textContent = '';
    return true;
  } catch {
    get('storage-error').textContent = 'Could not save in this browser. Storage may be unavailable or the notebook changed in another tab. Your draft is still here; copy it before reloading.';
    return false;
  }
}
function now() { const date = new Date(); if (timeOverride !== null) date.setHours(timeOverride, 0, 0, 0); return date; }
function render() {
  const advanced = advanceNight(life, now());
  if (advanced !== life) updateLife(advanced);
  const time = worldTime(now());
  const hibernating = isHibernating(life, now());
  const sleeping = hibernating || isNightSleeping(life, now());
  const currentStage = stage(life);
  world.dataset.realm = realm;
  world.dataset.stage = currentStage;
  world.dataset.sleeping = String(sleeping);
  world.dataset.hatchStep = String(life.hatchStep);
  audio.setSnoring(currentStage === 'adult' && sleeping);
  world.style.setProperty('--night', String(1 - time.daylight));
  world.dataset.environment = time.environment;
  const asset = currentStage === 'egg' ? `/assets/${life.hatchStep === 2 ? 'egg-cracked' : 'egg'}.svg` : `/assets/${sleeping ? 'obu-sleeping' : 'obu'}.svg`;
  if (creature.querySelector('img')!.getAttribute('src') !== asset) creature.querySelector('img')!.src = asset;
  creature.setAttribute('aria-label', currentStage === 'egg' ? 'Touch the egg to hatch OBU' : sleeping ? 'Wake OBU' : 'Pet OBU');
  get('hint').textContent = currentStage === 'egg' ? life.hatchStep === 0 ? 'What is this orb?' : 'It looks like something is ready to hatch.' : !life.greeted ? 'OBU has hatched! Greet OBU.' : hibernating ? 'OBU is hibernating. Touch to wake.' : '';
  get('hint').hidden = !get('hint').textContent;
  get('awaken').hidden = !sleeping;
  const wait = thoughtWait(life, now());
  get('growth-note').textContent = currentStage === 'baby' && life.credits.length === 0 ? 'OBU is still a baby, so answers are simple. Give OBU eight thoughts to help OBU grow. A new thought can be shared every ten minutes.' : currentStage === 'baby' && life.credits.length < 8 ? `${life.credits.length} of 8 thoughts received. OBU is growing curious.` : '';
  if (wait > 0 && currentStage !== 'egg' && !previewing) get('growth-note').textContent += ` Next thought in ${Math.ceil(wait / 60000)} min. You can write a draft now.`;
  get('growth-note').hidden = !get('growth-note').textContent;
  get('seed-tray').hidden = !seedAvailable(life);
  get('give-seed').hidden = !selectedSeed;
  get('growth-seed').setAttribute('aria-pressed', String(selectedSeed));
  get('realm-stones').hidden = currentStage !== 'adult';
  get('realm-note').hidden = currentStage !== 'adult' || !!life.dreamUnlockedAt;
  get<HTMLButtonElement>('dream-stone').disabled = !life.dreamUnlockedAt;
  get('dream-stone').title = life.dreamUnlockedAt ? 'Enter Dream' : 'Dream opens on OBU’s first night as an adult.';
  get('dream-stone').setAttribute('aria-label', life.dreamUnlockedAt ? 'Dream realm' : 'Dream realm — opens on the first Adult night');
  get('home-stone').setAttribute('aria-current', String(realm === 'home'));
  get('dream-stone').setAttribute('aria-current', String(realm === 'dream'));
  get<HTMLButtonElement>('thought').disabled = currentStage === 'egg' || previewing || !notebook;
  get<HTMLButtonElement>('record-dream').disabled = previewing || !notebook || !life.dreamUnlockedAt;
  get<HTMLSelectElement>('kind').querySelector<HTMLOptionElement>('[value="dream"]')!.disabled = !life.dreamUnlockedAt;
  get('dream').hidden = !life.dreamUnlockedAt;
  get('dream').textContent = realm === 'dream' ? 'Return Home ↗' : 'Enter Dream ↗';
  get('place').textContent = realm === 'dream' ? 'ON THE OTHER SIDE OF SLEEP' : 'SOMEWHERE, SOFTLY';
  get('time-label').textContent = realm === 'dream' ? '◌  Dream' : `${time.environment === 'day' ? '☼' : '☾'}  ${time.environment === 'day' ? 'Day' : 'Night'} · local time`;
  get('sound').textContent = audio.muted ? 'Sound off' : 'Sound on';
  get('sound').setAttribute('aria-pressed', String(!audio.muted));
  get('count').textContent = entries.length ? `(${entries.length})` : '';
  const current = entries.find(e => e.id === activeEntryId);
  const words = get<HTMLButtonElement>('few-words');
  words.hidden = !current || previewing || !!current.reflection?.utterance || current.reflection?.safety === 'imminent-danger';
  words.disabled = !!current && responseBusy.has(current.id);
  words.textContent = current && responseErrors.has(current.id) ? 'Try response again' : current?.type === 'dream' ? 'Explore this dream with OBU' : 'A few words?';
}
function react(name: string) {
  clearTimeout(reactionTimer);
  world.dataset.reaction = name;
  reactionTimer = setTimeout(() => { world.dataset.reaction = 'neutral'; }, 1800);
}
creature.addEventListener('click', () => {
  if (selectedSeed) { giveSeed(); return; }
  if (stage(life) === 'egg') {
    if (hatchLocked || !updateLife(touchEgg(life, now()))) return;
    hatchLocked = true;
    setTimeout(() => { hatchLocked = false; }, 900);
    react(life.hatchedAt ? 'hatching' : 'stirring');
    if (life.hatchedAt) void audio.play(true);
  }
  else {
    const sleeping = isHibernating(life, now()) || isNightSleeping(life, now());
    if (!updateLife({ ...wake(life, now()), greeted: true })) return;
    react(sleeping ? 'waking' : 'purr'); void audio.play(sleeping);
  }
  render();
});
get('awaken').addEventListener('click', () => { if (updateLife({ ...wake(life, now()), greeted: true })) { react('waking'); void audio.play(true); render(); } });
function giveSeed() {
  const next = feedSeed(life, now(), zone());
  if (next === life || !updateLife(next)) return;
  selectedSeed = false; react('hatching'); render(); creature.focus();
}
get('growth-seed').addEventListener('click', () => { selectedSeed = !selectedSeed; render(); });
get('give-seed').addEventListener('click', giveSeed);
// Pointer capture supports touch and mouse dragging; the button alternative supports keyboards.
let seedDrag: { x: number; y: number; moved: boolean } | null = null;
const seed = get<HTMLButtonElement>('growth-seed');
seed.addEventListener('pointerdown', event => { if (event.button !== 0) return; seedDrag = { x: event.clientX, y: event.clientY, moved: false }; seed.setPointerCapture(event.pointerId); });
seed.addEventListener('pointermove', event => {
  if (!seedDrag) return;
  const x = event.clientX - seedDrag.x, y = event.clientY - seedDrag.y;
  seedDrag.moved ||= Math.hypot(x, y) > 8;
  if (seedDrag.moved) { seed.style.transform = `translate(${x}px, ${y}px)`; world.classList.add('seed-dragging'); }
});
function resetDrag() { seedDrag = null; seed.style.transform = ''; world.classList.remove('seed-dragging'); }
seed.addEventListener('pointerup', event => {
  if (!seedDrag) return;
  const moved = seedDrag.moved;
  const target = creature.getBoundingClientRect();
  resetDrag();
  if (moved) {
    seed.addEventListener('click', e => e.stopImmediatePropagation(), { capture: true, once: true });
    if (event.clientX >= target.left && event.clientX <= target.right && event.clientY >= target.top && event.clientY <= target.bottom) giveSeed();
  }
});
seed.addEventListener('pointercancel', resetDrag);
seed.addEventListener('lostpointercapture', resetDrag);
get('sound').addEventListener('click', async () => { await audio.toggle(); render(); });
function navigate(next: Realm) { if (next === 'dream' && !life.dreamUnlockedAt) return; realm = next; render(); }
get('dream').addEventListener('click', () => navigate(realm === 'home' ? 'dream' : 'home'));
get('home-stone').addEventListener('click', () => navigate('home'));
get('dream-stone').addEventListener('click', () => navigate('dream'));
function syncEntryMode() {
  const dream = get<HTMLSelectElement>('kind').value === 'dream';
  get('give-thought').hidden = dream;
}
get('kind').addEventListener('change', syncEntryMode);
function openEntry(type: string) { if (type === 'dream' && !life.dreamUnlockedAt) return; get<HTMLSelectElement>('kind').value = type; syncEntryMode(); entryDialog.showModal(); }
get('thought').addEventListener('click', () => openEntry(realm === 'dream' ? 'dream' : 'thought'));
document.querySelectorAll<HTMLButtonElement>('[data-close]').forEach(button => button.addEventListener('click', () => button.closest('dialog')!.close()));
get('record-dream').addEventListener('click', () => {
  libraryDialog.close();
  render(); openEntry('dream');
});
get('entry-form').addEventListener('submit', event => {
  event.preventDefault();
  const input = get<HTMLTextAreaElement>('entry-text');
  if (!input.value.trim()) { input.setCustomValidity('Add a few words first.'); input.reportValidity(); return; }
  const type = get<HTMLSelectElement>('kind').value;
  if (previewing || stage(life) === 'egg' || (type === 'dream' && !life.dreamUnlockedAt)) { get('save-error').textContent = 'This entry is not available in the current scene.'; return; }
  const wait = thoughtWait(life, now());
  if (type === 'thought' && wait > 0) { get('save-error').textContent = `Your draft is here. You can give OBU another thought in ${Math.ceil(wait / 60000)} min.`; return; }
  const mode = type === 'dream' || (event as SubmitEvent).submitter?.getAttribute('value') === 'keep' ? 'keep' : 'reflect';
  const entry: Entry = { id: crypto.randomUUID(), text: input.value.trim(), type, at: now().toISOString(), mode, revision: 1 };
  const nextLife = type === 'thought' ? receiveEntry(life, entry.id, now(), zone()) : life;
  entry.expression = expression(nextLife);
  if (!persist(nextLife, [...entries, entry])) { get('save-error').textContent = previewing ? 'Reload to leave scene preview before saving.' : get('storage-error').textContent; return; }
  get('save-error').textContent = '';
  input.value = ''; entryDialog.close(); react('receiving');
  activeEntryId = entry.id;
  get('utterance').textContent = '';
  get('safety-support').hidden = true;
  get('response-status').textContent = 'Saved in this browser’s notebook.';
  render();
  if (mode === 'reflect') void requestWords(entry.id, false);
});
get('entry-text').addEventListener('input', () => get<HTMLTextAreaElement>('entry-text').setCustomValidity(''));
function renderLibrary() {
  const container = get('entries'); container.replaceChildren();
  if (!entries.length) { const empty = document.createElement('p'); empty.className = 'empty'; empty.textContent = 'An open page. No hurry to fill it.'; container.append(empty); }
  for (const entry of [...entries].reverse()) {
    const article = document.createElement('article');
    const label = document.createElement('small'); label.textContent = `${entry.type} · ${new Date(entry.at).toLocaleString()}`;
    const words = document.createElement('p'); words.textContent = entry.text;
    article.append(label, words);
    if (entry.reflection) {
      const response = document.createElement('p'); response.className = 'library-reflection';
      response.textContent = entry.reflection.support ?? entry.reflection.utterance ?? 'OBU received this quietly.';
      article.append(response);
    }
    const error = responseErrors.get(entry.id);
    if (error) { const message = document.createElement('p'); message.className = 'note'; message.textContent = error; article.append(message); }
    if (!entry.reflection?.utterance && entry.reflection?.safety !== 'imminent-danger') {
      const button = document.createElement('button'); button.className = 'quiet';
      button.textContent = responseBusy.has(entry.id) ? 'Receiving…' : error ? 'Try response again' : entry.type === 'dream' ? 'Explore this dream with OBU' : 'A few words?';
      button.disabled = responseBusy.has(entry.id) || previewing;
      button.addEventListener('click', () => { activeEntryId = entry.id; void requestWords(entry.id, true); });
      const permission = document.createElement('p'); permission.className = 'note';
      permission.textContent = 'Sends only this entry to your local AI server.';
      article.append(button, permission);
    }
    container.append(article);
  }
}
get('notebook').addEventListener('click', () => { renderLibrary(); libraryDialog.showModal(); });
if (import.meta.env.DEV) {
  get('preview-time').addEventListener('change', event => { const value = (event.target as HTMLSelectElement).value; timeOverride = value === 'live' ? null : Number(value); previewing = true; render(); });
  get('preview-stage').addEventListener('change', event => {
    const value = (event.target as HTMLSelectElement).value;
    previewing = true;
    life = createLife(crypto.randomUUID()); realm = 'home';
    if (value !== 'egg') life = hatch(life, now());
    const count = value === 'adult' ? 8 : value === 'fragment' ? 4 : 0;
    life.credits = Array.from({ length: count }, (_, i) => ({ entryId: `preview-${i}`, at: now().toISOString(), day: 'preview', timeZone: 'UTC' }));
    if (value === 'adult') life = feedSeed(life, now(), zone());
    selectedSeed = false;
    get('utterance').textContent = ''; render();
  });
  get('preview-sleep').addEventListener('click', () => {
    previewing = true;
    const past = new Date(now().getTime() - WEEK).toISOString();
    life = { ...life, hatchedAt: past, lastEntryAt: null, lastWakeAt: null }; render();
  });
  get('preview-voice').addEventListener('click', () => {
    if (stage(life) === 'egg') return;
    get('utterance').textContent = { word: 'Elsewhere.', fragment: 'A little fog, still.', utterance: 'OBU slept. The world continued.' }[expression(life)]; react('speaking');
  });
  const preview = document.querySelector('.dev')!;
  const seedPreview = document.createElement('button'); seedPreview.textContent = 'Preview eighth thought / seed';
  seedPreview.addEventListener('click', () => {
    previewing = true; realm = 'home'; selectedSeed = false;
    activeEntryId = null; get('response-status').textContent = ''; get('utterance').textContent = ''; get('safety-support').hidden = true;
    life = hatch(createLife('preview-seed'), now());
    life.greeted = true;
    for (let i = 0; i < 8; i++) life = receiveEntry(life, `preview-${i}`, new Date(now().getTime() - (7 - i) * THOUGHT_INTERVAL), zone());
    render();
  });
  preview.append(seedPreview);
}
let clockTimer: ReturnType<typeof setInterval>;
function resumeClock() { clearInterval(clockTimer); if (!document.hidden) { render(); clockTimer = setInterval(render, 1000); } }
document.addEventListener('visibilitychange', () => { world.classList.toggle('paused', document.hidden); if (document.hidden) { clearInterval(clockTimer); audio.suspend(); } else resumeClock(); });
window.addEventListener('focus', resumeClock);
resumeClock();
