-- Persist the AI companions created with each universe and use that party when
-- a campaign starts. This replaces the temporary hard-coded Echo companion.

alter table public.worlds
  add column if not exists starter_companions jsonb not null default '[]'::jsonb;

alter table public.worlds
  drop constraint if exists worlds_starter_companions_array;
alter table public.worlds
  add constraint worlds_starter_companions_array check (
    jsonb_typeof(starter_companions) = 'array'
    and jsonb_array_length(starter_companions) <= 3
  );

-- Ready drafts created before companions were part of WorldStarter inherit
-- three universe-specific people that the coauthor already generated.
update public.coauthor_sessions session
set generated_world = jsonb_set(
  session.generated_world,
  '{companions}',
  coalesce((
    select jsonb_agg(
      jsonb_build_object(
        'name', npc.value->>'name',
        'concept', left(npc.value->>'summary', 600),
        'abilities', case npc.position
          when 1 then '{"str":10,"dex":14,"con":11,"int":13,"wis":12,"cha":10}'::jsonb
          when 2 then '{"str":13,"dex":10,"con":14,"int":10,"wis":12,"cha":11}'::jsonb
          else '{"str":9,"dex":12,"con":11,"int":14,"wis":13,"cha":12}'::jsonb
        end,
        'hp', 10 + npc.position,
        'maxHp', 10 + npc.position,
        'defense', 11 + npc.position,
        'proficiency', 2,
        'inventory', jsonb_build_array('Universe field kit', 'Travel supplies'),
        'persona', jsonb_build_object(
          'voice', left(npc.value->>'summary', 240),
          'motivation', left('Pursue the goal behind: ' || (npc.value->>'summary'), 300),
          'flaw', 'Their personal agenda can complicate the party''s plans.',
          'bond', left('They belong to ' || (session.generated_world->>'title') || ' and choose to stand with the party.', 300)
        )
      ) order by npc.position
    )
    from jsonb_array_elements(coalesce(session.generated_world->'npcs', '[]'::jsonb))
      with ordinality as npc(value, position)
    where npc.position <= 3
  ), '[]'::jsonb),
  true
),
updated_at = now()
where session.generated_world is not null
  and jsonb_typeof(session.generated_world->'npcs') = 'array'
  and coalesce(jsonb_array_length(session.generated_world->'companions'), 0) = 0;

-- Confirmed universes do not retain the original generation JSON. Build their
-- starter companions from the AI-created NPC entities already stored for them.
update public.worlds world
set starter_companions = coalesce((
  select jsonb_agg(
    jsonb_build_object(
      'name', npc.name,
      'concept', left(npc.summary, 600),
      'abilities', case npc.position
        when 1 then '{"str":10,"dex":14,"con":11,"int":13,"wis":12,"cha":10}'::jsonb
        when 2 then '{"str":13,"dex":10,"con":14,"int":10,"wis":12,"cha":11}'::jsonb
        else '{"str":9,"dex":12,"con":11,"int":14,"wis":13,"cha":12}'::jsonb
      end,
      'hp', 10 + npc.position,
      'maxHp', 10 + npc.position,
      'defense', 11 + npc.position,
      'proficiency', 2,
      'inventory', jsonb_build_array('Universe field kit', 'Travel supplies'),
      'persona', jsonb_build_object(
        'voice', left(npc.summary, 240),
        'motivation', left('Pursue the goal behind: ' || npc.summary, 300),
        'flaw', 'Their personal agenda can complicate the party''s plans.',
        'bond', left('They belong to ' || world.title || ' and choose to stand with the party.', 300)
      )
    ) order by npc.position
  )
  from (
    select entity.name, entity.summary,
      (row_number() over (order by entity.created_at, entity.id))::integer as position
    from public.world_entities entity
    where entity.world_id = world.id and entity.kind = 'npc'
    order by entity.created_at, entity.id
    limit 3
  ) npc
), '[]'::jsonb)
where jsonb_array_length(world.starter_companions) = 0;

create or replace function public.stage_two_confirm_world(
  p_session_id uuid,
  p_owner_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
  v_session public.coauthor_sessions%rowtype;
  v_world jsonb;
  v_world_id uuid;
  v_entity jsonb;
begin
  select * into v_session from public.coauthor_sessions
  where id = p_session_id and owner_id = p_owner_id for update;
  if not found then raise exception 'coauthor session not found'; end if;
  if v_session.status = 'confirmed' and v_session.confirmed_world_id is not null then
    return v_session.confirmed_world_id;
  end if;
  if v_session.status <> 'ready' or v_session.generated_world is null then
    raise exception 'coauthor session is not ready to confirm';
  end if;
  v_world := v_session.generated_world;
  if coalesce(v_world->>'title', '') = ''
     or coalesce(v_world->>'openingScene', '') = ''
     or jsonb_typeof(v_world->'companions') <> 'array'
     or jsonb_array_length(v_world->'companions') <> 3 then
    raise exception 'generated world is incomplete';
  end if;

  insert into public.worlds(
    owner_id, title, genre, tone, premise, content_boundaries,
    hidden_threat, hook, opening_scene, starter_companions, status
  ) values (
    p_owner_id, v_world->>'title', v_world->>'genre', v_world->>'tone', v_world->>'premise',
    coalesce(v_world->'contentBoundaries', '[]'::jsonb), v_world->>'hiddenThreat',
    v_world->>'hook', v_world->>'openingScene', v_world->'companions', 'confirmed'
  ) returning id into v_world_id;

  v_entity := v_world->'startingLocation';
  insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
  values (v_world_id, 'location', v_entity->>'name', v_entity->>'summary',
    array(select jsonb_array_elements_text(coalesce(v_entity->'tags', '[]'::jsonb))),
    coalesce(v_entity->'secrets', '[]'::jsonb));
  for v_entity in select * from jsonb_array_elements(v_world->'npcs') loop
    insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
    values (v_world_id, 'npc', v_entity->>'name', v_entity->>'summary',
      array(select jsonb_array_elements_text(coalesce(v_entity->'tags', '[]'::jsonb))),
      coalesce(v_entity->'secrets', '[]'::jsonb));
  end loop;
  for v_entity in select * from jsonb_array_elements(v_world->'factions') loop
    insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
    values (v_world_id, 'faction', v_entity->>'name', v_entity->>'summary',
      array(select jsonb_array_elements_text(coalesce(v_entity->'tags', '[]'::jsonb))),
      coalesce(v_entity->'secrets', '[]'::jsonb));
  end loop;
  insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
  values (v_world_id, 'quest', 'Opening Hook', v_world->>'hook', array['opening'], '[]'::jsonb);

  update public.coauthor_sessions
  set status = 'confirmed', confirmed_world_id = v_world_id, updated_at = now()
  where id = p_session_id;
  return v_world_id;
end;
$$;

create or replace function public.stage_two_create_campaign(
  p_world_id uuid,
  p_owner_id uuid,
  p_title text
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
  v_world public.worlds%rowtype;
  v_campaign_id uuid;
  v_owner_name text;
  v_companion jsonb;
begin
  if p_title is null or char_length(btrim(p_title)) not between 3 and 100 then
    raise exception 'campaign title must be between 3 and 100 characters';
  end if;
  select * into v_world from public.worlds
  where id = p_world_id and owner_id = p_owner_id and status = 'confirmed' for share;
  if not found then raise exception 'confirmed world not found'; end if;
  if jsonb_typeof(v_world.starter_companions) <> 'array'
     or jsonb_array_length(v_world.starter_companions) = 0 then
    raise exception 'confirmed world has no AI companions';
  end if;

  select nullif(btrim(display_name), '') into v_owner_name
  from public.profiles where id = p_owner_id;
  v_owner_name := left(coalesce(v_owner_name, 'Wayfinder'), 80);

  insert into public.campaigns(world_id, owner_id, title, current_scene, next_prompt, status)
  values (p_world_id, p_owner_id, btrim(p_title), coalesce(v_world.opening_scene, v_world.premise), 'What do you do?', 'active')
  returning id into v_campaign_id;
  insert into public.campaign_members(campaign_id, user_id, role)
  values (v_campaign_id, p_owner_id, 'owner');

  insert into public.characters(
    campaign_id, user_id, name, concept, controller, abilities, hp, max_hp,
    defense, proficiency, inventory, statuses, persona
  ) values (
    v_campaign_id,
    p_owner_id,
    v_owner_name,
    left('An adaptable protagonist ready to confront the opening mystery of ' || v_world.title || '.', 600),
    'human'::public.character_controller,
    '{"str":10,"dex":12,"con":12,"int":11,"wis":13,"cha":10}'::jsonb,
    12,
    12,
    12,
    2,
    '["Field kit","Personal keepsake"]'::jsonb,
    '[]'::jsonb,
    jsonb_build_object(
      'voice', 'Defined by the player.',
      'motivation', 'Discover what the opening scene is hiding.',
      'flaw', 'Still learning what this world demands.',
      'bond', 'Protect the party through the first danger.'
    )
  );

  for v_companion in select * from jsonb_array_elements(v_world.starter_companions) loop
    insert into public.characters(
      campaign_id, user_id, name, concept, controller, abilities, hp, max_hp,
      defense, proficiency, inventory, statuses, persona
    ) values (
      v_campaign_id,
      null,
      v_companion->>'name',
      v_companion->>'concept',
      'ai'::public.character_controller,
      v_companion->'abilities',
      (v_companion->>'hp')::integer,
      (v_companion->>'maxHp')::integer,
      (v_companion->>'defense')::integer,
      (v_companion->>'proficiency')::integer,
      v_companion->'inventory',
      '[]'::jsonb,
      v_companion->'persona'
    );
  end loop;

  insert into public.rounds(campaign_id, number) values (v_campaign_id, 1);
  return v_campaign_id;
end;
$$;

-- Remove only the exact temporary Echo inserted by schema v10, preserving any
-- independently authored character that happens to share the name.
delete from public.characters character
where character.user_id is null
  and character.controller = 'ai'::public.character_controller
  and character.name = 'Echo'
  and character.inventory = '["Survey kit","Emergency supplies"]'::jsonb
  and character.persona->>'voice' = 'Observant, concise, and quietly curious.'
  and character.persona->>'motivation' = 'Help the party understand this unfamiliar world.'
  and character.concept like 'A persistent AI companion shaped by the %';

-- Existing active campaigns receive the companion roster belonging to their
-- universe. Name matching keeps this migration idempotent around manual heroes.
insert into public.characters(
  campaign_id, user_id, name, concept, controller, abilities, hp, max_hp,
  defense, proficiency, inventory, statuses, persona
)
select
  campaign.id,
  null,
  companion.value->>'name',
  companion.value->>'concept',
  'ai'::public.character_controller,
  companion.value->'abilities',
  (companion.value->>'hp')::integer,
  (companion.value->>'maxHp')::integer,
  (companion.value->>'defense')::integer,
  (companion.value->>'proficiency')::integer,
  companion.value->'inventory',
  '[]'::jsonb,
  companion.value->'persona'
from public.campaigns campaign
join public.worlds world on world.id = campaign.world_id
cross join lateral jsonb_array_elements(world.starter_companions) companion(value)
where campaign.status = 'active'
  and not exists (
    select 1 from public.characters character
    where character.campaign_id = campaign.id
      and character.controller = 'ai'::public.character_controller
      and lower(character.name) = lower(companion.value->>'name')
  );

create or replace function public.dng_schema_version()
returns integer language sql stable security definer set search_path = '' as $$
  select 11;
$$;

revoke all on function public.dng_schema_version() from public, anon, authenticated;
grant execute on function public.dng_schema_version() to service_role;

notify pgrst, 'reload schema';
