/**
 * Non-side-effect class export for `<agent-reasoning-ui>`.
 *
 * Importing this file gives you the class(es) without auto-registering the tag.
 * Useful for test isolation, subclassing with tag-name override, or selective
 * composition.
 *
 * The auto-register path stays at `@adia-ai/web-components/components/agent-reasoning`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <agent-reasoning-ui> — Agent inner monologue + pipeline viewer.
 *
 * Unified chain-of-thought surface. Composes <timeline-ui size="sm"> for step
 * rendering and adds plan / thought / iteration interstitials. Auto-collapses
 * after finish.
 *
 *   - STEPS     → timeline-ui size="sm" rows (completed / active+spinner / error)
 *   - THOUGHTS  → free-form monologue paragraphs
 *   - PLAN      → ordered up-front list in a subtle panel
 *   - ITERATION → attempt N blocks, each with its own sub-timeline
 *
 * JS API:
 *   const r = document.createElement('agent-reasoning-ui');
 *
 *   r.setPlan(['Read intent', 'Search patterns', 'Synthesize output']);
 *   r.addThought('The request matches the "product card" pattern.');
 *
 *   r.addStep({ id: 'search', label: 'search: Finding patterns…' });
 *   r.updateStep('search', { outcomes: ['3 candidates ranked'] });
 *   r.completeStep('search', 1200);
 *
 *   r.startIteration(1, 'First attempt');
 *   r.addStep({ id: 'gen', label: 'generate' });
 *   r.completeStep('gen', 3000);
 *   r.endIteration(1, { status: 'error', summary: 'validation 62/100' });
 *
 *   r.startIteration(2, 'Retry with hint');
 *   r.addStep({ id: 'gen2', label: 'generate (retry)' });
 *   r.completeStep('gen2', 2500);
 *   r.endIteration(2, { status: 'done', summary: 'validation 94/100' });
 *
 *   r.finish('2 iterations · 94/100 · 7s');
 *
 * Attributes:
 *   collapsed         — start collapsed
 *   no-autocollapse   — disable auto-collapse 1.2s after finish (default: false — auto-collapse on)
 *   heading           — fallback summary label while active
 *   status        — 'idle' | 'active' | 'done' | 'error' (default: 'active')
 *                   Drives the status icon: spinner / check-circle / warning-circle
 *
 * Events:
 *   reasoning-toggle       — summary expanded/collapsed (detail: { collapsed })
 *   reasoning-step-toggle  — a step's outcomes were toggled (detail: { stepId })
 *   reasoning-finish       — .finish() was called (detail: { summary, status })
 */

import { UIElement } from '../../core/element.js';

// Status lifecycle for the reasoning surface as a whole.
//   active  — still thinking (default; renders the spinner)
//   done    — finished successfully (renders check-circle)
//   error   — finished with failure (renders warning)
//   idle    — not started (renders muted dot)
const REASONING_STATUSES = new Set(['idle', 'active', 'done', 'error']);

function escapeText(s) {
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function formatDuration(ms) {
  if (ms < 1000) return ms + 'ms';
  return Math.round(ms / 1000) + 's';
}

export class UIAgentReasoning extends UIElement {
  // §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
  // consumer markup). Aggregated by installIconLoadersForRegistered()
  // across all defined elements. Audited by check-required-icons.mjs
  // (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
  static requiredIcons = ['check-circle', 'circle', 'warning', 'warning-circle'];

  static properties = {
    collapsed:       { type: Boolean, default: false,    reflect: true },
    noAutocollapse:  { type: Boolean, default: false,    reflect: true, attribute: 'no-autocollapse' },
    heading:         { type: String,  default: '',       reflect: true },
    status:          { type: String,  default: 'active', reflect: true },
  };

  static template = () => null;

  // Flat entries list; each is one of:
  //   { kind: 'step',       id, label, status, duration, outcomes, children }
  //   { kind: 'thought',    id, text }
  //   { kind: 'plan',       id, items: string[] }
  //   { kind: 'iteration',  id, n, label, status, summary, steps: step[] }
  #entries = [];
  #iterationStack = [];
  #startTime = Date.now();
  #timerInterval = null;
  #finishTimer = null;
  #summaryEl = null;
  #bodyEl = null;
  #bound = false;
  #resizeObserver = null;
  #spineRaf = 0;

  get entries() { return this.#entries; }

  connected() {
    if (this.#bound) return;
    this.#bound = true;
    this.#startTime = Date.now();
    this.#timerInterval = setInterval(() => this.#tickTimers(), 1000);
    this.#buildShell();
    // Re-measure the connector spine when row heights shift (text reflow on
    // resize moves the dot Ys). The spine writes only absolutely-positioned
    // ::before vars, so observing the host can't feed back into a resize loop.
    if (typeof ResizeObserver !== 'undefined') {
      this.#resizeObserver = new ResizeObserver(() => {
        if (this.#spineRaf) return;
        this.#spineRaf = requestAnimationFrame(() => {
          this.#spineRaf = 0;
          this.#layoutSpine();
        });
      });
      this.#resizeObserver.observe(this);
    }
  }

  // Single delegated handler for every nested timeline-item's toggle event.
  // Each step element carries its id on dataset.stepId so the handler can
  // resolve which step fired without per-item closures.
  #onStepToggle = (e) => {
    const item = e.target.closest('timeline-item-ui[data-step-id]');
    if (!item || !this.#bodyEl?.contains(item)) return;
    this.dispatchEvent(new CustomEvent('reasoning-step-toggle', {
      bubbles: true, detail: { stepId: item.dataset.stepId },
    }));
  };

  disconnected() {
    if (this.#timerInterval != null) {
      clearInterval(this.#timerInterval);
      this.#timerInterval = null;
    }
    if (this.#finishTimer != null) {
      clearTimeout(this.#finishTimer);
      this.#finishTimer = null;
    }
    if (this.#spineRaf) {
      cancelAnimationFrame(this.#spineRaf);
      this.#spineRaf = 0;
    }
    this.#resizeObserver?.disconnect();
    this.#resizeObserver = null;
    this.#summaryEl?.removeEventListener('click', this.#onSummaryClick);
    this.#summaryEl?.removeEventListener('keydown', this.#onSummaryKey);
    this.#bodyEl?.removeEventListener('timeline-toggle', this.#onStepToggle);
    this.#summaryEl = null;
    this.#bodyEl = null;
    this.#bound = false;
  }

  // ── Public API ──

  addStep(step) {
    const entry = {
      kind: 'step',
      status: 'active',
      duration: null,
      outcomes: [],
      children: [],
      ...step,
    };
    this.#appendToCurrentScope(entry);
    this.#render();
    return entry;
  }

  updateStep(id, updates) {
    const step = this.#findStep(id);
    if (!step) return;
    Object.assign(step, updates);
    this.#render();
  }

  completeStep(id, duration) {
    const step = this.#findStep(id);
    if (!step) return;
    step.status = 'done';
    if (duration != null) step.duration = duration;
    this.#render();
  }

  failStep(id, error) {
    const step = this.#findStep(id);
    if (!step) return;
    step.status = 'error';
    if (error) step.outcomes = [...(step.outcomes || []), String(error)];
    this.#render();
  }

  addThought(text, id = `thought-${this.#entries.length}`) {
    this.#appendToCurrentScope({ kind: 'thought', id, text });
    this.#render();
  }

  setPlan(items, id = 'plan') {
    const existing = this.#entries.find(e => e.kind === 'plan' && e.id === id);
    if (existing) existing.items = items.slice();
    else this.#entries.push({ kind: 'plan', id, items: items.slice() });
    this.#render();
  }

  startIteration(n, label = `Attempt ${n}`, id = `iter-${n}`) {
    const iter = {
      kind: 'iteration',
      id, n, label,
      status: 'active',
      summary: '',
      steps: [],
    };
    this.#entries.push(iter);
    this.#iterationStack.push(id);
    this.#render();
    return iter;
  }

  endIteration(n, { status = 'done', summary = '' } = {}) {
    const id = `iter-${n}`;
    const iter = this.#entries.find(e => e.kind === 'iteration' && e.id === id);
    if (!iter) return;
    iter.status = status;
    iter.summary = summary;
    const idx = this.#iterationStack.lastIndexOf(id);
    if (idx >= 0) this.#iterationStack.splice(idx, 1);
    this.#render();
  }

  finish(summaryLabel, { status = 'done' } = {}) {
    const resolved = REASONING_STATUSES.has(status) ? status : 'done';
    const isError = resolved === 'error';
    for (const entry of this.#entries) {
      if (entry.kind === 'step' && entry.status === 'active') {
        entry.status = isError ? 'error' : 'done';
      }
      if (entry.kind === 'iteration') {
        if (entry.status === 'active') entry.status = isError ? 'error' : 'done';
        for (const s of entry.steps || []) {
          if (s.status === 'active') s.status = isError ? 'error' : 'done';
        }
      }
    }
    if (this.#timerInterval != null) {
      clearInterval(this.#timerInterval);
      this.#timerInterval = null;
    }
    this.status = resolved;
    if (summaryLabel) this.dataset.summary = summaryLabel;
    this.#render();
    this.dispatchEvent(new CustomEvent('reasoning-finish', {
      bubbles: true,
      detail: { summary: summaryLabel || '', status: resolved },
    }));

    if (this.#finishTimer != null) clearTimeout(this.#finishTimer);
    if (!this.noAutocollapse && !isError) {
      this.#finishTimer = setTimeout(() => {
        this.#finishTimer = null;
        this.collapsed = true;
        this.#render();
      }, 1200);
    }
  }

  fail(summaryLabel) {
    this.finish(summaryLabel, { status: 'error' });
  }

  // ── Private ──

  #appendToCurrentScope(entry) {
    if (this.#iterationStack.length > 0) {
      const topId = this.#iterationStack[this.#iterationStack.length - 1];
      const iter = this.#entries.find(e => e.kind === 'iteration' && e.id === topId);
      if (iter) {
        if (entry.kind === 'step') iter.steps.push(entry);
        else this.#entries.push(entry);
        return;
      }
    }
    this.#entries.push(entry);
  }

  #findStep(id) {
    for (const e of this.#entries) {
      if (e.kind === 'step' && e.id === id) return e;
      if (e.kind === 'iteration') {
        for (const s of e.steps || []) if (s.id === id) return s;
      }
    }
    return null;
  }

  #onSummaryClick = () => {
    this.collapsed = !this.collapsed;
    this.dispatchEvent(new CustomEvent('reasoning-toggle', {
      bubbles: true,
      detail: { collapsed: this.collapsed },
    }));
    this.#render();
  };

  // Keyboard activation — Space/Enter toggle (matches role="button" semantics).
  #onSummaryKey = (e) => {
    if (e.key === ' ' || e.key === 'Enter') {
      e.preventDefault();
      this.#onSummaryClick();
    }
  };

  #buildShell() {
    this.innerHTML = '';

    // Summary is a click-to-toggle row; mark up + key-activate so it
    // behaves like a button under keyboard nav too.
    this.#summaryEl = document.createElement('div');
    this.#summaryEl.setAttribute('data-reasoning-summary', '');
    this.#summaryEl.setAttribute('role', 'button');
    this.#summaryEl.setAttribute('tabindex', '0');
    this.#summaryEl.setAttribute('aria-expanded', String(!this.collapsed));
    this.#summaryEl.addEventListener('click', this.#onSummaryClick);
    this.#summaryEl.addEventListener('keydown', this.#onSummaryKey);
    this.appendChild(this.#summaryEl);

    this.#bodyEl = document.createElement('div');
    this.#bodyEl.setAttribute('data-reasoning-body', '');
    this.#bodyEl.addEventListener('timeline-toggle', this.#onStepToggle);
    this.appendChild(this.#bodyEl);

    this.#render();
  }

  #activeLabel() {
    for (let i = this.#entries.length - 1; i >= 0; i--) {
      const e = this.#entries[i];
      if (e.kind === 'step' && e.status === 'active') return e.label;
      if (e.kind === 'iteration') {
        for (let j = (e.steps || []).length - 1; j >= 0; j--) {
          const s = e.steps[j];
          if (s.status === 'active') return s.label;
        }
      }
      if (e.kind === 'thought') return e.text;
    }
    const lastStep = this.#entries.findLast?.(e => e.kind === 'step');
    return lastStep?.label || this.heading || 'Thinking…';
  }

  #renderStatusIcon(status) {
    switch (status) {
      case 'done':
        return '<icon-ui name="check-circle" color="success" data-reasoning-check></icon-ui>';
      case 'error':
        return '<icon-ui name="warning-circle" color="danger" data-reasoning-check data-error></icon-ui>';
      case 'idle':
        return '<icon-ui name="circle" color="muted" data-reasoning-check data-idle></icon-ui>';
      case 'active':
      default:
        return '<span data-reasoning-spinner></span>';
    }
  }

  #countSteps() {
    let total = 0, done = 0;
    const tally = (s) => { total++; if (s.status === 'done') done++; };
    for (const e of this.#entries) {
      if (e.kind === 'step') tally(e);
      if (e.kind === 'iteration') for (const s of e.steps || []) tally(s);
    }
    return { total, done };
  }

  #render() {
    if (!this.#summaryEl) return;

    const status = REASONING_STATUSES.has(this.status) ? this.status : 'active';
    const isFinal = status === 'done' || status === 'error';
    const { total, done } = this.#countSteps();
    const elapsed = Math.round((Date.now() - this.#startTime) / 1000);
    const label = isFinal && this.dataset.summary ? this.dataset.summary : this.#activeLabel();

    this.#summaryEl.innerHTML = `
      ${this.#renderStatusIcon(status)}
      <span data-reasoning-label>${escapeText(label)}</span>
      <span data-reasoning-meta>
        ${total ? `<span data-reasoning-counter>${done}/${total}</span>` : ''}
        <span data-reasoning-time>${elapsed}s</span>
        <icon-ui name="${this.collapsed ? 'caret-right' : 'caret-up'}" color="muted" data-reasoning-caret></icon-ui>
      </span>
    `;

    this.#bodyEl.hidden = this.collapsed;
    this.#summaryEl.setAttribute('aria-expanded', String(!this.collapsed));
    if (this.collapsed) return;

    // Body rendering: we group contiguous steps into timeline-ui blocks and
    // render plan/thought/iteration as sibling nodes between them. Keeping
    // steps contiguous preserves timeline-ui's connector line.
    this.#bodyEl.innerHTML = '';
    let currentTimeline = null;
    for (const entry of this.#entries) {
      if (entry.kind === 'step') {
        if (!currentTimeline) {
          currentTimeline = this.#makeTimeline();
          this.#bodyEl.appendChild(currentTimeline);
        }
        currentTimeline.appendChild(this.#makeStepItem(entry));
      } else {
        currentTimeline = null;
        this.#bodyEl.appendChild(this.#renderNonStep(entry));
      }
    }

    this.#layoutSpine();
  }

  // Draw one continuous connector line down the marker column from the first
  // step dot to the last — bridging plan / thought / iteration interstitials
  // that otherwise fragment the per-block timeline connectors into stubs. The
  // endpoints are measured (not pure CSS) because interstitial row heights are
  // dynamic; the per-item connectors are suppressed in CSS (the spine owns the
  // line). No spine for 0–1 dots (nothing to connect → no trailing line).
  #layoutSpine() {
    const body = this.#bodyEl;
    if (!body) return;
    if (this.collapsed) { body.removeAttribute('data-spine'); return; }
    const items = body.querySelectorAll('timeline-item-ui');
    if (items.length < 2) { body.removeAttribute('data-spine'); return; }

    const bodyTop = body.getBoundingClientRect().top;
    // Dot center within an item = top:(1.4em − dot)/2 + 1px + dot/2 = 0.7em + 1px
    // (the dot size cancels), where em is the item's computed font-size.
    const dotY = (el) => {
      const r = el.getBoundingClientRect();
      const fs = parseFloat(getComputedStyle(el).fontSize) || 14;
      return (r.top - bodyTop) + 0.7 * fs + 1;
    };
    const top = dotY(items[0]);
    const height = Math.max(0, dotY(items[items.length - 1]) - top);
    body.style.setProperty('--reasoning-spine-top', `${Math.round(top)}px`);
    body.style.setProperty('--reasoning-spine-h', `${Math.round(height)}px`);
    body.setAttribute('data-spine', '');
  }

  #makeTimeline() {
    const tl = document.createElement('timeline-ui');
    tl.setAttribute('size', 'sm');
    return tl;
  }

  #makeStepItem(step) {
    const item = document.createElement('timeline-item-ui');
    item.setAttribute('text', step.label || '');
    // Map step.status → canonical timeline-item-ui [status] enum.
    // The legacy [completed]/[active]/[error] Booleans were removed
    // in the Phase 6 cut (2026-04-27); only [status] is honoured.
    if (step.status === 'done')   item.setAttribute('status', 'completed');
    if (step.status === 'error')  item.setAttribute('status', 'error');
    if (step.status === 'active') { item.setAttribute('status', 'active'); item.setAttribute('spinner', ''); }

    const duration = step.durationLabel
      || (step.duration != null ? formatDuration(step.duration) : '');
    if (duration) item.setAttribute('duration', duration);

    if (step.outcomes?.length) item.outcomes = step.outcomes;

    // Step id stamped on dataset so the parent's delegated #onStepToggle
    // listener (wired in #buildShell) can resolve which step fired.
    item.dataset.stepId = step.id || '';
    return item;
  }

  #renderNonStep(entry) {
    switch (entry.kind) {
      case 'thought':   return this.#renderThought(entry);
      case 'plan':      return this.#renderPlan(entry);
      case 'iteration': return this.#renderIteration(entry);
      default:          return document.createTextNode('');
    }
  }

  #renderThought(entry) {
    const el = document.createElement('div');
    el.setAttribute('data-reasoning-thought', '');
    const text = document.createElement('span');
    text.setAttribute('data-reasoning-thought-text', '');
    text.textContent = entry.text || '';
    el.appendChild(text);
    return el;
  }

  #renderPlan(entry) {
    const el = document.createElement('div');
    el.setAttribute('data-reasoning-plan', '');
    const label = document.createElement('div');
    label.setAttribute('data-reasoning-plan-label', '');
    label.textContent = 'Plan';
    el.appendChild(label);
    const list = document.createElement('ol');
    list.setAttribute('data-reasoning-plan-list', '');
    for (const item of (entry.items || [])) {
      const li = document.createElement('li');
      li.textContent = item;
      list.appendChild(li);
    }
    el.appendChild(list);
    return el;
  }

  #renderIteration(entry) {
    const el = document.createElement('div');
    el.setAttribute('data-reasoning-iteration', '');
    if (entry.status === 'active') el.setAttribute('data-active', '');
    if (entry.status === 'done')   el.setAttribute('data-done', '');
    if (entry.status === 'error')  el.setAttribute('data-error', '');

    const header = document.createElement('div');
    header.setAttribute('data-reasoning-iter-header', '');
    const statusIcon = entry.status === 'done'
      ? '<icon-ui name="check-circle" color="success" size="sm"></icon-ui>'
      : entry.status === 'error'
        ? '<icon-ui name="warning" color="danger" size="sm"></icon-ui>'
        : '<span data-reasoning-spinner data-small></span>';
    header.innerHTML = `
      ${statusIcon}
      <span data-reasoning-iter-n>#${entry.n}</span>
      <span data-reasoning-iter-label>${escapeText(entry.label || '')}</span>
      ${entry.summary ? `<span data-reasoning-iter-summary>${escapeText(entry.summary)}</span>` : ''}
    `;
    el.appendChild(header);

    if (entry.steps?.length) {
      const tl = this.#makeTimeline();
      for (const s of entry.steps) tl.appendChild(this.#makeStepItem(s));
      el.appendChild(tl);
    }
    return el;
  }

  #tickTimers() {
    if (this.status === 'done' || this.status === 'error') return;
    const elapsed = Math.round((Date.now() - this.#startTime) / 1000);
    const timeEl = this.#summaryEl?.querySelector('[data-reasoning-time]');
    if (timeEl) timeEl.textContent = elapsed + 's';

    // Live-update active step durations inside nested timeline-item-ui
    const elapsedLabel = elapsed + 's';
    for (const item of this.querySelectorAll('timeline-item-ui[active]:not([completed])')) {
      // Only mirror the ticking label; do not stomp a durationLabel author set
      if (!item.dataset._frozenDuration) {
        item.setAttribute('duration', elapsedLabel);
      }
    }
  }
}
