/* Adapt an API pipeline run (PipelineRunResult) to the rich SAMPLE shape that
   the design screens consume. Strategy: start from the fallback SAMPLE, then
   overlay every field the API has populated. Missing rich fields (character
   appearance, costume, props, relationships) keep the fallback's defaults so
   the UI never has empty cells. */

(function () {
  function clone(x) { return JSON.parse(JSON.stringify(x)); }

  function pickStr(x, fallback) {
    return (typeof x === 'string' && x.length > 0) ? x : fallback;
  }

  function adaptApiRunToSample(apiRun, fallback) {
    if (!apiRun || !fallback) return fallback;

    const out = clone(fallback);
    const gf  = (apiRun.brief && apiRun.brief.genre_fields) || {};
    const bgf = (apiRun.bible && apiRun.bible.genre_fields) || {};

    // ---- project ----
    out.project = {
      ...out.project,
      id: pickStr(apiRun.project?.id, out.project.id),
      title: pickStr(apiRun.project?.title, out.project.title),
      concept: pickStr(apiRun.brief?.concept, out.project.concept),
      runtime_target: apiRun.project?.target_runtime_seconds ?? out.project.runtime_target,
      runtime_estimate: (apiRun.shots ?? []).reduce((acc, s) => acc + (s.duration_seconds || 0), 0) || out.project.runtime_estimate,
      genre: pickStr(apiRun.project?.genre_plugin_id, out.project.genre),
      subgenre: pickStr(gf.horror_subgenre, out.project.subgenre),
      canon_version: pickStr(apiRun.canon?.canon_version, out.project.canon_version),
    };

    // ---- brief ----
    out.brief = {
      ...out.brief,
      logline: pickStr(apiRun.brief?.logline, out.brief.logline),
      fear_engine: pickStr(gf.fear_engine, out.brief.fear_engine),
      threat_type: pickStr(gf.threat_type, out.brief.threat_type),
      threat_visibility: pickStr(gf.threat_visibility, out.brief.threat_visibility),
      dread_style: pickStr(gf.dread_style, out.brief.dread_style),
      violence_level: pickStr(gf.violence_level, out.brief.violence_level),
      gore_level: pickStr(gf.gore_level, out.brief.gore_level),
      ending_type: pickStr(gf.ending_type, out.brief.ending_type),
      avoid: Array.isArray(gf.avoid) && gf.avoid.length ? gf.avoid : out.brief.avoid,
    };

    // ---- characters: API treatments → SAMPLE rich shape ----
    if (Array.isArray(apiRun.characters) && apiRun.characters.length) {
      out.characters = apiRun.characters.map((c, i) => {
        const fallbackChar = out.characters[i] ?? out.characters[0];
        return {
          ...fallbackChar,
          id: pickStr(c.id, fallbackChar.id),
          name: pickStr(c.name, fallbackChar.name),
          archetype: pickStr(c.archetype, fallbackChar.archetype),
          motivation: pickStr(c.motivation, fallbackChar.motivation),
          vulnerability: pickStr(c.vulnerability, fallbackChar.vulnerability),
          voice: pickStr(c.voice, fallbackChar.voice),
          summary: pickStr(c.appearance, fallbackChar.summary),
        };
      });
    }

    // ---- threat ----
    if (apiRun.threat) {
      out.threat = {
        ...out.threat,
        id: pickStr(apiRun.threat.id, out.threat.id),
        name: pickStr(apiRun.threat.name, out.threat.name),
        type: pickStr(apiRun.threat.type, out.threat.type),
        motivation: pickStr(apiRun.threat.motivation, out.threat.motivation),
        visibility_strategy: pickStr(apiRun.threat.visibility_strategy, out.threat.visibility_strategy),
        escalation: pickStr(apiRun.threat.escalation_pattern, out.threat.escalation),
        // PRD §18 ThreatProfile uses `limitations`; the SAMPLE shape uses `rule` (which originally meant
        // the bible's horror_rule). Surface both so editors can target the right field.
        rule: pickStr(bgf.horror_rule, out.threat.rule),
        limitations: pickStr(apiRun.threat.limitations, out.threat.limitations || ''),
      };
    }

    // ---- location (use first as the SAMPLE 'location' singleton) ----
    const firstLoc = Array.isArray(apiRun.locations) && apiRun.locations[0];
    if (firstLoc) {
      out.location = {
        ...out.location,
        id: pickStr(firstLoc.id, out.location.id),
        name: pickStr(firstLoc.name, out.location.name),
        summary: pickStr(firstLoc.description, out.location.summary),
        spatial_notes: pickStr(firstLoc.spatial_notes, out.location.spatial_notes || ''),
        continuity_notes: Array.isArray(firstLoc.continuity_notes) && firstLoc.continuity_notes.length
          ? firstLoc.continuity_notes
          : (out.location.continuity_notes || []),
        motifs: Array.isArray(bgf.visual_motifs) && bgf.visual_motifs.length ? bgf.visual_motifs : out.location.motifs,
      };
    }
    // Surface the full locations list separately so the multi-location editor can read it.
    if (Array.isArray(apiRun.locations)) {
      out._locations = apiRun.locations;
    }
    // Surface the live bible so MotifsBuilder, ForbiddenBuilder, WorldRulesBuilder can edit genre_fields.
    if (apiRun.bible) {
      out._bible = apiRun.bible;
    }

    // ---- outline (scene cards) ----
    if (Array.isArray(apiRun.scenes) && apiRun.scenes.length) {
      let cursor = 0;
      out.outline = apiRun.scenes.map((s, i) => {
        const start = cursor;
        const dur = s.duration_seconds || 30;
        cursor += dur;
        const sgf = s.genre_fields || {};
        const peak = sgf.tension_curve?.peak ?? 5;
        const start_v = sgf.tension_curve?.start ?? 1;
        const release = sgf.tension_curve?.release ?? 3;
        const curve = [start_v, start_v + 1, start_v + 2, peak - 1, peak, peak - 1, release];
        return {
          id: s.id,
          n: i + 1,
          title: s.title || `Scene ${i + 1}`,
          in: start,
          dur,
          function: pickStr(sgf.horror_function, 'dread_build'),
          curve,
          beat: s.summary || '',
          finalframe: pickStr(sgf.final_frame, ''),
          // Full PRD §12.5 sub-object surfaced for the SceneCardEditor.
          _genre: sgf,
        };
      });
    }

    // ---- shots ----
    if (Array.isArray(apiRun.shots) && apiRun.shots.length) {
      const sceneIdToN = new Map(out.outline.map((sc, i) => [sc.id, i + 1]));
      const stalePackets = apiRun.stale_packets || {};
      out.shots = apiRun.shots.map((sh, i) => ({
        // SAMPLE-shaped fields (used by Filmstrip card + Table view core columns)
        id: sh.id,
        scene: sh.scene_id,
        n: i + 1,
        dur: sh.duration_seconds,
        type: pickStr(sh.shot_type, 'static_medium'),
        desc: pickStr(sh.subject || sh.action, ''),
        camera: pickStr(sh.camera, ''),
        visibility: pickStr(sh.horror_modifiers?.threat_visibility, 'unseen'),
        // Underscore-prefixed fields are the rest of PRD §12.7 ShotPlan, surfaced
        // for the Filmstrip detail editor and the Table view's Action column.
        _action: pickStr(sh.action, ''),
        _composition: pickStr(sh.composition, ''),
        _lighting: pickStr(sh.lighting, ''),
        _motion: pickStr(sh.motion, ''),
        _sound: pickStr(sh.sound, ''),
        _horror_modifiers: sh.horror_modifiers || {},
        _prompt_stale: Boolean(stalePackets[sh.id]),
      }));
    }

    // ---- packet (use first video packet for the prompts editor) ----
    const firstPacket = apiRun.prompts?.videoPackets?.[0];
    if (firstPacket) {
      out.packet = {
        ...out.packet,
        shot_id: pickStr(firstPacket.shot_id, out.packet.shot_id),
        scene_id: pickStr(firstPacket.scene_id, out.packet.scene_id),
        duration_seconds: firstPacket.duration_seconds || out.packet.duration_seconds,
        aspect_ratio: pickStr(firstPacket.aspect_ratio, out.packet.aspect_ratio),
        visual_style: pickStr(firstPacket.visual_style, out.packet.visual_style),
        subject: pickStr(firstPacket.subject, out.packet.subject),
        setting: pickStr(firstPacket.setting, out.packet.setting),
        action: pickStr(firstPacket.action, out.packet.action),
        camera: pickStr(firstPacket.camera, out.packet.camera),
        lens: pickStr(firstPacket.lens, out.packet.lens),
        composition: pickStr(firstPacket.composition, out.packet.composition),
        lighting: pickStr(firstPacket.lighting, out.packet.lighting),
        color_palette: pickStr(firstPacket.color_palette, out.packet.color_palette),
        motion: pickStr(firstPacket.motion, out.packet.motion),
        continuity_refs: firstPacket.continuity_refs?.length ? firstPacket.continuity_refs : out.packet.continuity_refs,
        negative_constraints: firstPacket.negative_constraints?.length ? firstPacket.negative_constraints : out.packet.negative_constraints,
        dialogue_line_ids: firstPacket.dialogue_line_ids ?? [],
        sound_cue_ids: firstPacket.sound_cue_ids ?? [],
        genre_modifiers: { ...(out.packet.genre_modifiers || {}), ...(firstPacket.genre_modifiers || {}) },
        provider_params: {
          ...(out.packet.provider_params || {}),
          ...(firstPacket.provider_params || {}),
        },
      };
    }

    // ---- cost ----
    if (apiRun.cost_estimate) {
      const c = apiRun.cost_estimate;
      out.cost = {
        ...out.cost,
        estimate_id: pickStr(c.estimate_id, out.cost.estimate_id),
        total_shots: c.total_shots ?? out.cost.total_shots,
        total_seconds: c.total_estimated_video_seconds ?? out.cost.total_seconds,
        resolution: pickStr(c.resolution, out.cost.resolution),
        provider: pickStr(c.provider, out.cost.provider),
        route: pickStr(c.route, out.cost.route),
        input_mix: c.input_mix ?? out.cost.input_mix,
        estimated_provider_cost_usd: c.estimated_provider_cost_usd ?? out.cost.estimated_provider_cost_usd,
        estimated_partner_audio_cost_usd: c.estimated_partner_audio_cost_usd ?? out.cost.estimated_partner_audio_cost_usd,
        estimated_total_cost_usd: c.estimated_total_cost_usd ?? out.cost.estimated_total_cost_usd,
        pricing_basis: pickStr(c.pricing_notes?.[0], out.cost.pricing_basis),
      };
    }

    // ---- validation (overlay if present) ----
    if (apiRun.validation && apiRun.validation.layers) {
      const layerToCheck = (key, label) => {
        const layer = apiRun.validation.layers[key];
        if (!layer) return null;
        const kind = layer.errors.length ? 'bad' : layer.warnings.length ? 'warn' : 'ok';
        const detail = layer.errors[0] || layer.warnings[0] || `${label} clean.`;
        return { kind, label, detail: typeof detail === 'string' ? detail : detail.message || String(detail) };
      };
      const arr = [
        layerToCheck('schema',       'Schema valid'),
        layerToCheck('completeness', 'Prompt completeness'),
        layerToCheck('canon',        'Canon continuity'),
        layerToCheck('qa',           'Horror QA'),
        layerToCheck('provider',     'Provider constraints'),
        layerToCheck('rights',       'Rights / likeness'),
        layerToCheck('safety',       'Safety constraints'),
      ].filter(Boolean);
      if (arr.length) out.validation = arr;
    }

    // ---- live mode badge ----
    out._live = true;
    out._live_project_id = apiRun.project?.id;

    // ---- raw screenplay payload for the live script editor ----
    if (apiRun.screenplay) {
      out._api_screenplay = {
        scenes: apiRun.screenplay.scenes ?? [],
        lines:  apiRun.screenplay.lines  ?? [],
      };
    }
    if (Array.isArray(apiRun.characters)) {
      out._api_characters = apiRun.characters;
    }

    // ---- audio cues (per-scene) ----
    out._cues = {
      music:    apiRun.prompts?.musicCues    ?? [],
      sfx:      apiRun.prompts?.sfxCues      ?? [],
      ambience: apiRun.prompts?.ambienceCues ?? [],
    };

    // ---- reference assets (per-entity) ----
    // Pull the latest character_reference / location_reference per entity so
    // the UI can render thumbnails next to characters and locations without
    // an extra round-trip.
    const apiAssets = Array.isArray(apiRun.assets) ? apiRun.assets : null;
    if (apiAssets) {
      const latestByEntity = (type) => {
        const map = {};
        for (const a of apiAssets) {
          if (a.type !== type) continue;
          const cur = map[a.linked_entity_id];
          if (!cur || new Date(a.created_at) > new Date(cur.created_at)) map[a.linked_entity_id] = a;
        }
        return map;
      };
      out._refs = {
        character: latestByEntity('character_reference'),
        location:  latestByEntity('location_reference'),
      };
      // Decorate characters with their cloned voice id (extras.elevenlabs_voice_id).
      if (Array.isArray(apiRun.characters)) {
        out._character_voice = {};
        for (const c of apiRun.characters) {
          const vid = c.extras && c.extras.elevenlabs_voice_id;
          if (vid) out._character_voice[c.id] = vid;
        }
      }
    }

    return out;
  }

  // Adapt poll results from /api/batches/:id/status into SAMPLE.jobs shape.
  function adaptApiJobsToSample(apiJobs, fallbackJobs) {
    if (!Array.isArray(apiJobs) || !apiJobs.length) return fallbackJobs;
    return apiJobs.map((j) => ({
      id:       j.shot_id || j.id,
      status:   j.status || 'queued',
      progress: Math.round(((j.progress ?? 0) * 100)),
      eta:      j.status === 'complete' ? '—' : j.eta ?? '—',
      cost:     0, // server-side per-job cost lookup is a TODO
      asset:    j.asset_id,
      error:    j.error,
    }));
  }

  window.adaptApiRunToSample  = adaptApiRunToSample;
  window.adaptApiJobsToSample = adaptApiJobsToSample;
})();
