-- Introduce a versioned SRD 5.2.1 rules profile without rebuilding campaigns.
-- Existing narrative, rounds, HP, inventory and statuses remain untouched.

alter table public.campaigns
  add column if not exists ruleset_version smallint;

insert into public.audit_entries(
  campaign_id, actor_id, action, entity_type, entity_id, before_state, after_state
)
select
  campaign.id,
  campaign.owner_id,
  'migrate_ruleset',
  'campaign',
  campaign.id,
  jsonb_build_object('rulesetVersion', coalesce(campaign.ruleset_version, 1)),
  jsonb_build_object(
    'rulesetVersion', 2,
    'ruleset', 'SRD 5.2.1 alpha',
    'preserved', jsonb_build_array('rounds', 'history', 'hp', 'inventory', 'statuses')
  )
from public.campaigns campaign
where coalesce(campaign.ruleset_version, 1) < 2;

update public.campaigns
set ruleset_version = 2
where ruleset_version is null or ruleset_version < 2;

alter table public.campaigns
  alter column ruleset_version set default 2,
  alter column ruleset_version set not null;

alter table public.campaigns
  drop constraint if exists campaigns_ruleset_version_supported;
alter table public.campaigns
  add constraint campaigns_ruleset_version_supported check (ruleset_version = 2);

alter table public.characters
  add column if not exists rules_state jsonb;

-- Compatibility is deliberate: migrated heroes retain the old planner hint
-- only until their profile is explicitly edited. Newly-authored profiles use
-- server-owned skill/save lists and set this flag to false.
update public.characters character
set rules_state = jsonb_build_object(
  'version', 2,
  'level', case
    when character.proficiency <= 2 then 1
    when character.proficiency = 3 then 5
    when character.proficiency = 4 then 9
    when character.proficiency = 5 then 13
    else 17
  end,
  'skillProficiencies', '[]'::jsonb,
  'skillExpertise', '[]'::jsonb,
  'savingThrowProficiencies', '[]'::jsonb,
  'temporaryHp', 0,
  'deathSaves', jsonb_build_object('successes', 0, 'failures', 0),
  'exhaustion', 0,
  'damageResistances', '[]'::jsonb,
  'damageVulnerabilities', '[]'::jsonb,
  'damageImmunities', '[]'::jsonb,
  'resources', jsonb_build_object(
    'hitDice', jsonb_build_object('current', 1, 'max', 1, 'recovery', 'longRest')
  ),
  'legacyProficiencyFallback', true
)
where character.rules_state is null;

alter table public.characters
  alter column rules_state set default '{
    "version":2,
    "level":1,
    "skillProficiencies":[],
    "skillExpertise":[],
    "savingThrowProficiencies":[],
    "temporaryHp":0,
    "deathSaves":{"successes":0,"failures":0},
    "exhaustion":0,
    "damageResistances":[],
    "damageVulnerabilities":[],
    "damageImmunities":[],
    "resources":{"hitDice":{"current":1,"max":1,"recovery":"longRest"}},
    "legacyProficiencyFallback":true
  }'::jsonb,
  alter column rules_state set not null;

alter table public.characters
  drop constraint if exists characters_rules_state_v2;
alter table public.characters
  add constraint characters_rules_state_v2 check (
    jsonb_typeof(rules_state) = 'object'
    and rules_state->>'version' = '2'
    and (rules_state->>'level')::integer between 1 and 20
    and jsonb_typeof(rules_state->'skillProficiencies') = 'array'
    and jsonb_typeof(rules_state->'skillExpertise') = 'array'
    and jsonb_typeof(rules_state->'savingThrowProficiencies') = 'array'
    and (rules_state->>'temporaryHp')::integer between 0 and 999
    and jsonb_typeof(rules_state->'deathSaves') = 'object'
    and (rules_state->'deathSaves'->>'successes')::integer between 0 and 3
    and (rules_state->'deathSaves'->>'failures')::integer between 0 and 3
    and (rules_state->>'exhaustion')::integer between 0 and 6
    and jsonb_typeof(rules_state->'damageResistances') = 'array'
    and jsonb_typeof(rules_state->'damageVulnerabilities') = 'array'
    and jsonb_typeof(rules_state->'damageImmunities') = 'array'
    and jsonb_typeof(rules_state->'resources') = 'object'
    and jsonb_typeof(rules_state->'legacyProficiencyFallback') = 'boolean'
  );

-- Older world drafts gain deterministic proficiency data so a newly-started
-- campaign is fully on rules v2 even when its universe predates this migration.
update public.worlds world
set starter_companions = coalesce((
  select jsonb_agg(
    companion.value || case companion.position
      when 1 then '{"skillProficiencies":["acrobatics","perception","stealth","survival"],"skillExpertise":[],"savingThrowProficiencies":["dex","wis"]}'::jsonb
      when 2 then '{"skillProficiencies":["athletics","intimidation","perception","survival"],"skillExpertise":[],"savingThrowProficiencies":["str","con"]}'::jsonb
      else '{"skillProficiencies":["arcana","history","investigation","perception"],"skillExpertise":["investigation"],"savingThrowProficiencies":["int","wis"]}'::jsonb
    end
    order by companion.position
  )
  from jsonb_array_elements(world.starter_companions) with ordinality as companion(value, position)
), '[]'::jsonb)
where jsonb_typeof(world.starter_companions) = 'array';

update public.coauthor_sessions session
set generated_world = jsonb_set(
  session.generated_world,
  '{companions}',
  coalesce((
    select jsonb_agg(
      companion.value || case companion.position
        when 1 then '{"skillProficiencies":["acrobatics","perception","stealth","survival"],"skillExpertise":[],"savingThrowProficiencies":["dex","wis"]}'::jsonb
        when 2 then '{"skillProficiencies":["athletics","intimidation","perception","survival"],"skillExpertise":[],"savingThrowProficiencies":["str","con"]}'::jsonb
        else '{"skillProficiencies":["arcana","history","investigation","perception"],"skillExpertise":["investigation"],"savingThrowProficiencies":["int","wis"]}'::jsonb
      end
      order by companion.position
    )
    from jsonb_array_elements(session.generated_world->'companions') with ordinality as companion(value, position)
  ), '[]'::jsonb),
  true
)
where session.generated_world is not null
  and jsonb_typeof(session.generated_world->'companions') = 'array';

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, ruleset_version)
  values (p_world_id, p_owner_id, btrim(p_title), coalesce(v_world.opening_scene, v_world.premise), 'What do you do?', 'active', 2)
  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, rules_state
  ) 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.'
    ),
    '{
      "version":2,"level":1,
      "skillProficiencies":["investigation","perception","persuasion","survival"],
      "skillExpertise":[],"savingThrowProficiencies":["dex","wis"],
      "temporaryHp":0,"deathSaves":{"successes":0,"failures":0},"exhaustion":0,
      "damageResistances":[],"damageVulnerabilities":[],"damageImmunities":[],
      "resources":{"hitDice":{"current":1,"max":1,"recovery":"longRest"}},
      "legacyProficiencyFallback":false
    }'::jsonb
  );

  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, rules_state
    ) 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',
      jsonb_build_object(
        'version', 2, 'level', 1,
        'skillProficiencies', coalesce(v_companion->'skillProficiencies', '[]'::jsonb),
        'skillExpertise', coalesce(v_companion->'skillExpertise', '[]'::jsonb),
        'savingThrowProficiencies', coalesce(v_companion->'savingThrowProficiencies', '[]'::jsonb),
        'temporaryHp', 0, 'deathSaves', jsonb_build_object('successes', 0, 'failures', 0),
        'exhaustion', 0, 'damageResistances', '[]'::jsonb,
        'damageVulnerabilities', '[]'::jsonb, 'damageImmunities', '[]'::jsonb,
        'resources', jsonb_build_object('hitDice', jsonb_build_object('current', 1, 'max', 1, 'recovery', 'longRest')),
        'legacyProficiencyFallback', false
      )
    );
  end loop;

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

create or replace function public.stage_five_create_character(
  p_campaign_id uuid,
  p_actor_id uuid,
  p_controller text,
  p_name text,
  p_concept text,
  p_abilities jsonb,
  p_hp integer,
  p_max_hp integer,
  p_defense integer,
  p_proficiency integer,
  p_inventory jsonb,
  p_statuses jsonb,
  p_persona jsonb,
  p_rules_state jsonb
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
  v_character_id uuid;
begin
  if coalesce(jsonb_typeof(p_rules_state), '') <> 'object' or p_rules_state->>'version' <> '2' then
    raise exception 'a version 2 rules profile is required';
  end if;
  v_character_id := public.stage_four_create_character(
    p_campaign_id, p_actor_id, p_controller, p_name, p_concept, p_abilities,
    p_hp, p_max_hp, p_defense, p_proficiency, p_inventory, p_statuses, p_persona
  );
  update public.characters set rules_state = p_rules_state where id = v_character_id;
  return v_character_id;
end;
$$;

create or replace function public.apply_srd_character_rules(
  p_round_id uuid,
  p_character_states jsonb
) returns void language plpgsql security definer set search_path = '' as $$
declare
  v_campaign_id uuid;
  v_state jsonb;
  v_character_id uuid;
  v_seen uuid[] := '{}';
begin
  select campaign_id into v_campaign_id from public.rounds where id = p_round_id;
  if not found then raise exception 'round not found'; end if;
  if coalesce(jsonb_typeof(p_character_states), '') <> 'array' then
    raise exception 'character states must be an array';
  end if;
  for v_state in select * from jsonb_array_elements(p_character_states) loop
    v_character_id := (v_state->>'id')::uuid;
    if v_character_id = any(v_seen) then raise exception 'duplicate character state for %', v_character_id; end if;
    v_seen := array_append(v_seen, v_character_id);
    if coalesce(jsonb_typeof(v_state->'rulesState'), '') <> 'object'
       or v_state->'rulesState'->>'version' <> '2' then
      raise exception 'character % has an invalid rules profile', v_character_id;
    end if;
    update public.characters
    set rules_state = v_state->'rulesState'
    where id = v_character_id and campaign_id = v_campaign_id;
    if not found then raise exception 'character % is outside the round campaign', v_character_id; end if;
  end loop;
end;
$$;

create or replace function public.commit_srd_round_resolution(
  p_round_id uuid,
  p_narration text,
  p_next_prompt text,
  p_rolls jsonb,
  p_events jsonb,
  p_character_states jsonb,
  p_memory jsonb,
  p_idempotency_key text
) returns void language plpgsql security definer set search_path = '' as $$
begin
  if exists (select 1 from public.rounds where id = p_round_id and status = 'resolved') then return; end if;
  perform public.commit_round_resolution(
    p_round_id, p_narration, p_next_prompt, p_rolls, p_events,
    p_character_states, p_memory, p_idempotency_key
  );
  perform public.apply_srd_character_rules(p_round_id, p_character_states);
end;
$$;

create or replace function public.commit_claimed_srd_round_resolution(
  p_round_id uuid,
  p_narration text,
  p_next_prompt text,
  p_rolls jsonb,
  p_events jsonb,
  p_character_states jsonb,
  p_memory jsonb,
  p_idempotency_key text,
  p_worker_id text
) returns void language plpgsql security definer set search_path = '' as $$
begin
  perform public.commit_claimed_round_resolution(
    p_round_id, p_narration, p_next_prompt, p_rolls, p_events,
    p_character_states, p_memory, p_idempotency_key, p_worker_id
  );
  perform public.apply_srd_character_rules(p_round_id, p_character_states);
end;
$$;

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

revoke all on function public.stage_five_create_character(uuid, uuid, text, text, text, jsonb, integer, integer, integer, integer, jsonb, jsonb, jsonb, jsonb) from public, anon, authenticated;
revoke all on function public.apply_srd_character_rules(uuid, jsonb) from public, anon, authenticated;
revoke all on function public.commit_srd_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text) from public, anon, authenticated;
revoke all on function public.commit_claimed_srd_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text) from public, anon, authenticated;
revoke all on function public.dng_schema_version() from public, anon, authenticated;
grant execute on function public.stage_five_create_character(uuid, uuid, text, text, text, jsonb, integer, integer, integer, integer, jsonb, jsonb, jsonb, jsonb) to service_role;
grant execute on function public.commit_srd_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text) to service_role;
grant execute on function public.commit_claimed_srd_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text) to service_role;
grant execute on function public.dng_schema_version() to service_role;

notify pgrst, 'reload schema';
