create extension if not exists pgcrypto;

create or replace function public.next_order_group_sms_check_at(
  start_from timestamptz default now(),
  delay interval default interval '0 minutes'
)
returns timestamptz
language plpgsql
stable
as $$
declare
  v_timezone text := 'Europe/Simferopol';
  v_local_time timestamp;
  v_local_date date;
  v_work_start timestamp;
  v_work_end timestamp;
  v_candidate timestamp;
begin
  v_local_time := (start_from at time zone v_timezone) + delay;
  v_local_date := v_local_time::date;
  v_work_start := v_local_date + time '09:00';
  v_work_end := v_local_date + time '20:00';

  if v_local_time < v_work_start then
    v_candidate := v_work_start;
  elsif v_local_time >= v_work_end then
    v_candidate := (v_local_date + 1) + time '09:00';
  else
    v_candidate := v_local_time;
  end if;

  return v_candidate at time zone v_timezone;
end;
$$;

create table if not exists public.roles (
  id uuid primary key default gen_random_uuid(),
  name text not null unique,
  permissions jsonb not null default '[]'::jsonb,
  created_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.users (
  id uuid primary key references auth.users (id) on delete cascade,
  email text not null unique,
  name text not null,
  role_id uuid not null references public.roles (id),
  last_login timestamptz,
  created_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.orders (
  id uuid primary key default gen_random_uuid(),
  order_number text not null unique,
  customer jsonb not null,
  status text not null,
  delivery_agreement_status text not null default 'Не начато',
  manager_id uuid references public.users (id),
  logistician_id uuid references public.users (id),
  assigned_driver_id uuid references public.users (id),
  ready_for_delivery_at timestamptz,
  delivery_flow_started_at timestamptz,
  delivery_flow_source text,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.order_logisticians (
  id uuid primary key default gen_random_uuid(),
  order_id uuid not null references public.orders (id) on delete cascade,
  logistician_id uuid not null references public.users (id) on delete cascade,
  assigned_at timestamptz not null default timezone('utc', now()),
  assigned_by uuid references public.users (id),
  unique (order_id, logistician_id)
);

create table if not exists public.order_history (
  id uuid primary key default gen_random_uuid(),
  order_id uuid not null references public.orders (id) on delete cascade,
  action text not null,
  old_status text,
  new_status text,
  user_id uuid references public.users (id),
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.delivery_slots (
  id uuid primary key default gen_random_uuid(),
  order_id uuid not null references public.orders (id) on delete cascade,
  delivery_date date not null,
  delivery_time text not null,
  logistician_id uuid references public.users (id),
  status text not null default 'pending_confirmation',
  selected_by_client_at timestamptz,
  created_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.chat_messages (
  id uuid primary key default gen_random_uuid(),
  order_id uuid not null references public.orders (id) on delete cascade,
  sender_name text,
  sender_type text not null check (sender_type in ('client', 'bot', 'operator', 'system')),
  channel text not null check (channel in ('telegram', 'vk', 'messenger_max', 'sms', 'email')),
  text text not null,
  external_message_id text,
  payload jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.error_logs (
  id uuid primary key default gen_random_uuid(),
  order_id uuid references public.orders (id) on delete set null,
  provider text,
  level text not null default 'error',
  message text not null,
  details jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.order_groups (
  id uuid primary key default gen_random_uuid(),
  group_key text not null,
  customer jsonb,
  order_numbers text[] not null default '{}',
  status text not null default 'ready_for_notification',
  delivery_status text not null default 'pending_confirmation',
  sms_sent_at timestamptz,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  created_from_exchange_at timestamptz,
  source_key text,
  customer_name text,
  customer_phone text,
  customer_phone_normalized text,
  customer_date text,
  orders_total integer,
  orders_ready integer,
  orders_not_ready integer,
  source_orders jsonb,
  delivery_invitation_id uuid,
  delivery_link text,
  notification_status text not null default 'not_started',
  sms_attempts integer not null default 0,
  first_sms_sent_at timestamptz,
  second_sms_sent_at timestamptz,
  last_sms_error text,
  next_notification_check_at timestamptz,
  delivery_date date,
  delivery_time text
);

create table if not exists public.delivery_invitations (
  id uuid primary key default gen_random_uuid(),
  order_id uuid references public.orders (id) on delete cascade unique,
  order_group_id uuid references public.order_groups (id) on delete cascade,
  token_hash text not null unique,
  state text not null default 'awaiting_choice',
  order_number text,
  customer_name text,
  customer_phone text,
  customer_messenger text,
  available_slots text[] not null default array['Первая половина дня', 'Вторая половина дня'],
  expires_at timestamptz,
  revoked_at timestamptz,
  access_count integer not null default 0,
  last_accessed_at timestamptz,
  delivery_date date,
  delivery_time text,
  sent_at timestamptz,
  opened_at timestamptz,
  confirmed_at timestamptz,
  logistics_transferred_at timestamptz,
  paid_storage_at timestamptz,
  delivered_at timestamptz,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.integration_events (
  id uuid primary key default gen_random_uuid(),
  order_id uuid references public.orders (id) on delete set null,
  event_type text not null,
  direction text not null default 'internal',
  source text not null default 'supabase-function',
  status text not null default 'success',
  payload jsonb not null default '{}'::jsonb,
  error_message text,
  created_at timestamptz not null default timezone('utc', now())
);

create table if not exists public.rate_limits (
  id uuid primary key default gen_random_uuid(),
  scope text not null,
  rate_key text not null,
  window_start timestamptz not null,
  count integer not null default 1,
  blocked_until timestamptz,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  unique (scope, rate_key, window_start)
);

alter table public.orders add column if not exists delivery_agreement_status text not null default 'Не начато';
alter table public.orders add column if not exists assigned_driver_id uuid references public.users (id);
alter table public.orders add column if not exists ready_for_delivery_at timestamptz;
alter table public.orders add column if not exists delivery_flow_started_at timestamptz;
alter table public.orders add column if not exists delivery_flow_source text;
alter table public.chat_messages drop constraint if exists chat_messages_channel_check;
alter table public.chat_messages
  add constraint chat_messages_channel_check
  check (channel in ('telegram', 'vk', 'messenger_max', 'sms', 'email'));

alter table public.delivery_invitations add column if not exists state text not null default 'awaiting_choice';
alter table public.delivery_invitations alter column order_id drop not null;
alter table public.delivery_invitations add column if not exists order_group_id uuid references public.order_groups(id) on delete cascade;
alter table public.delivery_invitations add column if not exists expires_at timestamptz;
alter table public.delivery_invitations add column if not exists revoked_at timestamptz;
alter table public.delivery_invitations add column if not exists access_count integer not null default 0;
alter table public.delivery_invitations add column if not exists last_accessed_at timestamptz;
alter table public.delivery_invitations add column if not exists delivery_date date;
alter table public.delivery_invitations add column if not exists delivery_time text;
alter table public.delivery_invitations add column if not exists sent_at timestamptz;
alter table public.delivery_invitations add column if not exists opened_at timestamptz;
alter table public.delivery_invitations add column if not exists confirmed_at timestamptz;
alter table public.delivery_invitations add column if not exists logistics_transferred_at timestamptz;
alter table public.delivery_invitations add column if not exists paid_storage_at timestamptz;
alter table public.delivery_invitations add column if not exists delivered_at timestamptz;
alter table public.delivery_invitations add column if not exists updated_at timestamptz not null default timezone('utc', now());

alter table public.order_groups add column if not exists delivery_invitation_id uuid references public.delivery_invitations(id) on delete set null;
alter table public.order_groups add column if not exists delivery_link text;
alter table public.order_groups add column if not exists notification_status text not null default 'not_started';
alter table public.order_groups add column if not exists sms_attempts integer not null default 0;
alter table public.order_groups add column if not exists first_sms_sent_at timestamptz;
alter table public.order_groups add column if not exists second_sms_sent_at timestamptz;
alter table public.order_groups add column if not exists last_sms_error text;
alter table public.order_groups add column if not exists next_notification_check_at timestamptz;
alter table public.order_groups add column if not exists delivery_date date;
alter table public.order_groups add column if not exists delivery_time text;

alter table public.orders add column if not exists source_order_number text;
alter table public.orders add column if not exists source_order_date date;
alter table public.orders add column if not exists source_customer_name text;
alter table public.orders add column if not exists source_customer_phone text;
alter table public.orders add column if not exists source_customer_email text;
alter table public.orders add column if not exists source_customer_city text;
alter table public.orders add column if not exists source_total_sum numeric;
alter table public.orders add column if not exists source_paid_at timestamptz;
alter table public.orders add column if not exists source_gateway text;
alter table public.orders add column if not exists source_associated_bills_text text;
alter table public.orders add column if not exists source_production_at timestamptz;
alter table public.orders add column if not exists source_saw_at timestamptz;
alter table public.orders add column if not exists source_glue_at timestamptz;
alter table public.orders add column if not exists source_h_glue_at timestamptz;
alter table public.orders add column if not exists source_curve_at timestamptz;
alter table public.orders add column if not exists source_accept_at timestamptz;
alter table public.orders add column if not exists source_ship_at timestamptz;
alter table public.orders add column if not exists source_payload jsonb;
alter table public.orders add column if not exists delivery_set_key text;
alter table public.orders add column if not exists delivery_set_name text;
alter table public.orders add column if not exists delivery_set_status text;
alter table public.orders add column if not exists delivery_set_ready_at timestamptz;
alter table public.orders add column if not exists delivery_ready_reason text;
alter table public.orders add column if not exists source_sms_legacy_at timestamptz;

comment on column public.orders.source_sms_legacy_at is 'Informational only: legacy 1C SMS timestamp. Must NOT be used to start new delivery automation scenarios.';

alter table public.integration_events add column if not exists direction text not null default 'internal';
alter table public.integration_events add column if not exists source text not null default 'supabase-function';
alter table public.integration_events add column if not exists status text not null default 'success';
alter table public.integration_events add column if not exists payload jsonb not null default '{}'::jsonb;
alter table public.integration_events add column if not exists error_message text;
alter table public.rate_limits add column if not exists scope text not null;
alter table public.rate_limits add column if not exists rate_key text not null;
alter table public.rate_limits add column if not exists window_start timestamptz not null;
alter table public.rate_limits add column if not exists count integer not null default 1;
alter table public.rate_limits add column if not exists blocked_until timestamptz;
alter table public.rate_limits add column if not exists created_at timestamptz not null default timezone('utc', now());
alter table public.rate_limits add column if not exists updated_at timestamptz not null default timezone('utc', now());

create index if not exists idx_orders_delivery_set_key on public.orders (delivery_set_key);
create index if not exists idx_orders_delivery_set_status on public.orders (delivery_set_status);
create index if not exists idx_orders_source_accept_at on public.orders (source_accept_at);
create index if not exists idx_orders_source_ship_at on public.orders (source_ship_at);

insert into public.roles (name, permissions)
values
  (
    'manager',
    '["orders.create","orders.update.own","orders.read.own","comments.manage"]'::jsonb
  ),
  (
    'production_lead',
    '["orders.read.all","production.queue.manage","orders.status.production"]'::jsonb
  ),
  (
    'logistician',
    '["orders.read.assigned","delivery.manage","chatbots.manage"]'::jsonb
  ),
  (
    'driver',
    '["orders.read.assigned_driver","orders.status.driver"]'::jsonb
  ),
  (
    'admin',
    '["*"]'::jsonb
  )
on conflict (name) do nothing;

create or replace function public.set_updated_at()
returns trigger
language plpgsql
as $$
begin
  new.updated_at = timezone('utc', now());
  return new;
end;
$$;

drop trigger if exists orders_set_updated_at on public.orders;
create trigger orders_set_updated_at
before update on public.orders
for each row
execute function public.set_updated_at();

drop trigger if exists delivery_invitations_set_updated_at on public.delivery_invitations;
create trigger delivery_invitations_set_updated_at
before update on public.delivery_invitations
for each row
execute function public.set_updated_at();

create or replace function public.current_role_name()
returns text
language sql
stable
security definer
set search_path = public
as $$
  select r.name
  from public.users u
  join public.roles r on r.id = u.role_id
  where u.id = auth.uid()
$$;

create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
declare
  default_role_id uuid;
begin
  select id
  into default_role_id
  from public.roles
  where name = coalesce(new.raw_user_meta_data ->> 'role', 'manager')
  limit 1;

  if default_role_id is null then
    select id into default_role_id from public.roles where name = 'manager' limit 1;
  end if;

  insert into public.users (id, email, name, role_id, last_login)
  values (
    new.id,
    new.email,
    coalesce(new.raw_user_meta_data ->> 'name', split_part(new.email, '@', 1)),
    default_role_id,
    timezone('utc', now())
  )
  on conflict (id) do update
  set email = excluded.email,
      last_login = timezone('utc', now());

  return new;
end;
$$;

drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created
after insert on auth.users
for each row
execute function public.handle_new_user();

create or replace function public.log_order_status_change()
returns trigger
language plpgsql
security definer
as $$
begin
  if tg_op = 'INSERT' then
    insert into public.order_history (order_id, action, old_status, new_status, user_id)
    values (new.id, 'Создан заказ', null, new.status, auth.uid());
    return new;
  end if;

  if old.status is distinct from new.status then
    insert into public.order_history (order_id, action, old_status, new_status, user_id)
    values (new.id, 'Изменение статуса', old.status, new.status, auth.uid());
  end if;

  if old.delivery_agreement_status is distinct from new.delivery_agreement_status then
    insert into public.order_history (order_id, action, old_status, new_status, user_id, metadata)
    values (
      new.id,
      'Изменение согласования доставки',
      old.delivery_agreement_status,
      new.delivery_agreement_status,
      auth.uid(),
      jsonb_build_object('scope', 'delivery_agreement')
    );
  end if;

  return new;
end;
$$;

drop trigger if exists orders_history_insert on public.orders;
create trigger orders_history_insert
after insert or update on public.orders
for each row
execute function public.log_order_status_change();

create index if not exists idx_users_role_id on public.users (role_id);
create index if not exists idx_orders_status on public.orders (status);
create index if not exists idx_orders_manager_id on public.orders (manager_id);
create index if not exists idx_orders_logistician_id on public.orders (logistician_id);
create index if not exists idx_orders_assigned_driver_id on public.orders (assigned_driver_id);
create index if not exists idx_orders_ready_for_delivery_at on public.orders (ready_for_delivery_at);
create index if not exists idx_orders_delivery_flow_started_at on public.orders (delivery_flow_started_at);
create index if not exists idx_orders_created_at on public.orders (created_at desc);
create index if not exists idx_order_logisticians_order_id on public.order_logisticians (order_id);
create index if not exists idx_order_logisticians_logistician_id on public.order_logisticians (logistician_id);
create index if not exists idx_order_history_order_id on public.order_history (order_id, created_at desc);
create index if not exists idx_delivery_slots_order_id on public.delivery_slots (order_id);
create index if not exists idx_delivery_slots_logistician_id on public.delivery_slots (logistician_id);
create index if not exists idx_delivery_slots_selected_by_client_at on public.delivery_slots (selected_by_client_at);
create index if not exists idx_chat_messages_order_id on public.chat_messages (order_id, created_at desc);
create index if not exists idx_chat_messages_external_message_id on public.chat_messages (external_message_id);
create unique index if not exists idx_chat_messages_channel_external_unique
on public.chat_messages (channel, external_message_id)
where external_message_id is not null;
create index if not exists idx_orders_search on public.orders using gin (
  to_tsvector(
    'simple',
    coalesce(order_number, '') || ' ' || coalesce(customer ->> 'name', '') || ' ' || coalesce(customer ->> 'phone', '')
  )
);
create index if not exists idx_chat_messages_search on public.chat_messages using gin (
  to_tsvector('russian', coalesce(text, ''))
);
create index if not exists idx_delivery_invitations_order_id on public.delivery_invitations (order_id);
create index if not exists idx_delivery_invitations_order_group_id on public.delivery_invitations (order_group_id);
create index if not exists idx_delivery_invitations_token_hash on public.delivery_invitations (token_hash);
create index if not exists idx_delivery_invitations_state on public.delivery_invitations (state);
create index if not exists idx_delivery_invitations_expires_at on public.delivery_invitations (expires_at);
create index if not exists idx_order_groups_status on public.order_groups (status);
create index if not exists idx_order_groups_delivery_status on public.order_groups (delivery_status);
create index if not exists idx_order_groups_notification_status on public.order_groups (notification_status, next_notification_check_at);
create index if not exists idx_integration_events_order_id on public.integration_events (order_id, created_at desc);
create index if not exists idx_integration_events_event_type on public.integration_events (event_type);
create index if not exists idx_rate_limits_scope_key_window on public.rate_limits (scope, rate_key, window_start desc);
create index if not exists idx_rate_limits_blocked_until on public.rate_limits (blocked_until);

create or replace function public.check_rate_limit(
  p_scope text,
  p_key text,
  p_max_count integer,
  p_window_seconds integer,
  p_block_seconds integer default 0
)
returns table (
  allowed boolean,
  current_count integer,
  limit_count integer,
  blocked_until timestamptz,
  window_start timestamptz
)
language plpgsql
security definer
set search_path = public
as $$
declare
  v_now timestamptz := timezone('utc', now());
  v_window_start timestamptz;
  v_count integer;
  v_blocked_until timestamptz;
begin
  if p_max_count <= 0 then
    raise exception 'max_count must be positive';
  end if;

  if p_window_seconds <= 0 then
    raise exception 'window_seconds must be positive';
  end if;

  v_window_start := to_timestamp(floor(extract(epoch from v_now) / p_window_seconds) * p_window_seconds);

  select rl.blocked_until
  into v_blocked_until
  from public.rate_limits rl
  where rl.scope = p_scope
    and rl.rate_key = p_key
    and rl.blocked_until is not null
    and rl.blocked_until > v_now
  order by rl.blocked_until desc
  limit 1;

  if v_blocked_until is not null then
    return query
    select false, 0, p_max_count, v_blocked_until, v_window_start;
    return;
  end if;

  insert into public.rate_limits (scope, rate_key, window_start, count, blocked_until)
  values (p_scope, p_key, v_window_start, 1, null)
  on conflict (scope, rate_key, window_start)
  do update set
    count = public.rate_limits.count + 1,
    blocked_until = case
      when public.rate_limits.count + 1 > p_max_count and p_block_seconds > 0 then greatest(
        coalesce(public.rate_limits.blocked_until, v_now),
        v_now + make_interval(secs => p_block_seconds)
      )
      else public.rate_limits.blocked_until
    end,
    updated_at = v_now
  returning count, blocked_until
  into v_count, v_blocked_until;

  return query
  select
    v_count <= p_max_count and (v_blocked_until is null or v_blocked_until <= v_now),
    v_count,
    p_max_count,
    v_blocked_until,
    v_window_start;
end;
$$;

create or replace function public.get_delivery_invitation_by_token(p_token text)
returns jsonb
language plpgsql
security definer
set search_path = public, extensions
as $$
declare
  v_invitation public.delivery_invitations%rowtype;
  v_group public.order_groups%rowtype;
  v_order record;
  v_token_hash text;
  v_state text;
  v_order_number text;
  v_customer_name text;
  v_customer_phone text;
  v_order_items jsonb;
  v_order_numbers jsonb;
  v_now timestamptz := timezone('utc', now());
begin
  if nullif(trim(coalesce(p_token, '')), '') is null then
    raise exception 'token is required';
  end if;

  v_token_hash := encode(digest(p_token, 'sha256'), 'hex');

  select *
  into v_invitation
  from public.delivery_invitations
  where token_hash = v_token_hash;

  if not found then
    raise exception 'Invitation not found';
  end if;

  if v_invitation.revoked_at is not null then
    raise exception 'Invitation expired';
  end if;

  if v_invitation.expires_at is not null and v_invitation.expires_at <= v_now then
    raise exception 'Invitation expired';
  end if;

  if v_invitation.order_group_id is not null then
    select *
    into v_group
    from public.order_groups
    where id = v_invitation.order_group_id;

    if not found then
      raise exception 'Order group not found';
    end if;

    v_state := case
      when v_group.delivery_status = 'agreed' then 'agreed'
      when v_group.delivery_status = 'delivered' then 'delivered'
      when v_invitation.state in ('awaiting_choice', 'opened', 'reminder_sent') then v_invitation.state
      else 'default'
    end;

    update public.delivery_invitations
    set
      opened_at = case
        when v_state in ('awaiting_choice', 'opened', 'reminder_sent') and opened_at is null then v_now
        else opened_at
      end,
      access_count = coalesce(access_count, 0) + 1,
      last_accessed_at = v_now
    where id = v_invitation.id
    returning * into v_invitation;

    v_order_number := coalesce(
      nullif(v_invitation.order_number, ''),
      to_jsonb(v_group.order_numbers) ->> 0,
      nullif(v_group.group_key, '')
    );
    v_customer_name := coalesce(
      nullif(v_group.customer_name, ''),
      nullif(v_group.customer ->> 'name', ''),
      nullif(v_invitation.customer_name, '')
    );
    v_customer_phone := coalesce(
      nullif(v_group.customer_phone, ''),
      nullif(v_group.customer ->> 'phone', ''),
      nullif(v_invitation.customer_phone, '')
    );
    select coalesce(
      jsonb_agg(jsonb_build_object('name', order_number, 'quantity', '')),
      '[]'::jsonb
    )
    into v_order_items
    from jsonb_array_elements_text(
      case
        when jsonb_typeof(to_jsonb(v_group.order_numbers)) = 'array' then to_jsonb(v_group.order_numbers)
        else '[]'::jsonb
      end
    ) as order_number;

    return jsonb_build_object(
      'ok', true,
      'invitation', jsonb_build_object(
        'orderId', coalesce(v_invitation.order_group_id, v_group.id)::text,
        'orderGroupId', coalesce(v_invitation.order_group_id, v_group.id)::text,
        'state', v_state,
        'token', p_token,
        'orderNumber', v_order_number,
        'customerName', v_customer_name,
        'customerPhone', v_customer_phone,
        'orderItems', v_order_items,
        'availableSlots', coalesce(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
        'deliveryDate', v_invitation.delivery_date,
        'deliveryTime', v_invitation.delivery_time,
        'orderStatus', null,
        'deliveryAgreementStatus', null
      )
    );
  end if;

  select id, order_number, status, delivery_agreement_status, customer
  into v_order
  from public.orders
  where id = v_invitation.order_id;

  if not found then
    raise exception 'Order not found';
  end if;

  v_state := case v_order.status
    when 'Ожидает ответа клиента' then 'awaiting_choice'
    when 'Ожидает согласования доставки' then 'opened'
    when 'Напоминание отправлено' then 'reminder_sent'
    when 'Переход отправлен' then 'reminder_sent'
    when 'Передан логисту' then 'transferred_to_logistics'
    when 'Платное хранение' then 'paid_storage'
    when 'Доставлен' then 'delivered'
    when 'Доставка согласована' then 'agreed'
    else 'default'
  end;

  update public.delivery_invitations
  set
    opened_at = case
      when v_state in ('awaiting_choice', 'opened', 'reminder_sent') and opened_at is null then v_now
      else opened_at
    end,
    access_count = coalesce(access_count, 0) + 1,
    last_accessed_at = v_now
  where id = v_invitation.id
  returning * into v_invitation;

  v_order_items := case
    when jsonb_typeof(v_order.customer -> 'items') = 'array' then v_order.customer -> 'items'
    else '[]'::jsonb
  end;

  return jsonb_build_object(
    'ok', true,
    'invitation', jsonb_build_object(
      'orderId', v_invitation.order_id::text,
      'state', v_state,
      'token', p_token,
      'orderNumber', coalesce(nullif(v_order.order_number, ''), nullif(v_invitation.order_number, '')),
      'customerName', coalesce(nullif(v_order.customer ->> 'name', ''), nullif(v_invitation.customer_name, '')),
      'customerPhone', coalesce(nullif(v_order.customer ->> 'phone', ''), nullif(v_invitation.customer_phone, '')),
      'orderItems', v_order_items,
      'availableSlots', coalesce(to_jsonb(v_invitation.available_slots), '[]'::jsonb),
      'deliveryDate', v_invitation.delivery_date,
      'deliveryTime', v_invitation.delivery_time,
      'orderStatus', v_order.status,
      'deliveryAgreementStatus', v_order.delivery_agreement_status
    )
  );
end;
$$;

create or replace function public.confirm_delivery_choice_by_token(
  p_token text,
  p_delivery_date date,
  p_delivery_time text
)
returns jsonb
language plpgsql
security definer
set search_path = public, extensions
as $$
declare
  v_invitation public.delivery_invitations%rowtype;
  v_group public.order_groups%rowtype;
  v_order record;
  v_token_hash text;
  v_slot_label text;
  v_now timestamptz := timezone('utc', now());
begin
  if nullif(trim(coalesce(p_token, '')), '') is null then
    raise exception 'token is required';
  end if;

  if p_delivery_date is null or nullif(trim(coalesce(p_delivery_time, '')), '') is null then
    raise exception 'Selected slot is not available';
  end if;

  v_token_hash := encode(digest(p_token, 'sha256'), 'hex');
  v_slot_label := concat(p_delivery_date::text, ', ', trim(p_delivery_time));

  select *
  into v_invitation
  from public.delivery_invitations
  where token_hash = v_token_hash
  for update;

  if not found then
    raise exception 'Invitation not found';
  end if;

  if v_invitation.revoked_at is not null then
    raise exception 'Invitation expired';
  end if;

  if v_invitation.expires_at is not null and v_invitation.expires_at <= v_now then
    raise exception 'Invitation expired';
  end if;

  if v_invitation.state not in ('awaiting_choice', 'opened', 'reminder_sent') then
    raise exception 'Invitation is no longer active';
  end if;

  if cardinality(v_invitation.available_slots) > 0 and not (v_slot_label = any(v_invitation.available_slots)) then
    raise exception 'Selected slot is not available';
  end if;

  if v_invitation.order_group_id is not null then
    select *
    into v_group
    from public.order_groups
    where id = v_invitation.order_group_id
    for update;

    if not found then
      raise exception 'Order group not found';
    end if;

    if v_group.delivery_status <> 'pending_confirmation' then
      raise exception 'Invitation is no longer active';
    end if;

    update public.delivery_invitations
    set
      state = 'agreed',
      delivery_date = p_delivery_date,
      delivery_time = trim(p_delivery_time),
      confirmed_at = v_now,
      access_count = coalesce(access_count, 0) + 1,
      last_accessed_at = v_now
    where id = v_invitation.id;

    update public.order_groups
    set
      delivery_status = 'agreed',
      delivery_date = p_delivery_date,
      delivery_time = trim(p_delivery_time),
      notification_status = 'confirmed',
      updated_at = v_now
    where id = v_group.id;

    insert into public.integration_events (
      order_id,
      event_type,
      direction,
      status,
      payload
    )
    values (
      null,
      'delivery_choice_confirmed',
      'inbound',
      'success',
      jsonb_build_object(
        'order_group_id', v_group.id,
        'delivery_invitation_id', v_invitation.id,
        'delivery_date', p_delivery_date,
        'delivery_time', trim(p_delivery_time)
      )
    );

    return jsonb_build_object(
      'ok', true,
      'orderGroupId', v_group.id,
      'deliveryStatus', 'agreed'
    );
  end if;

  select id, status, delivery_agreement_status
  into v_order
  from public.orders
  where id = v_invitation.order_id
  for update;

  if not found then
    raise exception 'Order not found';
  end if;

  if v_order.status not in ('Ожидает ответа клиента', 'Ожидает согласования доставки') then
    raise exception 'Invitation is no longer active';
  end if;

  update public.delivery_invitations
  set
    state = 'agreed',
    delivery_date = p_delivery_date,
    delivery_time = trim(p_delivery_time),
    confirmed_at = v_now,
    access_count = coalesce(access_count, 0) + 1,
    last_accessed_at = v_now
  where id = v_invitation.id;

  update public.orders
  set
    status = 'Доставка согласована',
    delivery_agreement_status = 'Подтверждено клиентом'
  where id = v_order.id;

  insert into public.delivery_slots (
    order_id,
    delivery_date,
    delivery_time,
    logistician_id,
    status
  )
  values (
    v_order.id,
    p_delivery_date,
    trim(p_delivery_time),
    null,
    'confirmed_by_client'
  );

  insert into public.order_history (
    order_id,
    action,
    old_status,
    new_status,
    metadata
  )
  values (
    v_order.id,
    'Подтверждение выбора доставки клиентом',
    v_order.status,
    'Доставка согласована',
    jsonb_build_object(
      'old_delivery_agreement_status', v_order.delivery_agreement_status,
      'new_delivery_agreement_status', 'Подтверждено клиентом',
      'delivery_date', p_delivery_date,
      'delivery_time', trim(p_delivery_time)
    )
  );

  insert into public.integration_events (
    order_id,
    event_type,
    direction,
    status,
    payload
  )
  values (
    v_order.id,
    'delivery_choice_confirmed',
    'inbound',
    'success',
    jsonb_build_object(
      'delivery_date', p_delivery_date,
      'delivery_time', trim(p_delivery_time)
    )
  );

  return jsonb_build_object(
    'ok', true,
    'orderId', v_order.id,
    'status', 'Доставка согласована',
    'deliveryAgreementStatus', 'Подтверждено клиентом'
  );
end;
$$;

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

revoke all on function public.confirm_delivery_choice_by_token(text, date, text) from public;
grant execute on function public.confirm_delivery_choice_by_token(text, date, text) to anon, authenticated;

alter table public.roles enable row level security;
alter table public.users enable row level security;
alter table public.orders enable row level security;
alter table public.order_logisticians enable row level security;
alter table public.order_history enable row level security;
alter table public.delivery_slots enable row level security;
alter table public.chat_messages enable row level security;
alter table public.error_logs enable row level security;
alter table public.order_groups enable row level security;
alter table public.delivery_invitations enable row level security;
alter table public.integration_events enable row level security;

drop policy if exists "roles select authenticated" on public.roles;
create policy "roles select authenticated" on public.roles
for select
using (public.current_role_name() is not null);

drop policy if exists "roles admin mutate" on public.roles;
create policy "roles admin mutate" on public.roles
for insert
with check (public.current_role_name() = 'admin');

drop policy if exists "roles admin update" on public.roles;
create policy "roles admin update" on public.roles
for update
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');

drop policy if exists "roles admin delete" on public.roles;
create policy "roles admin delete" on public.roles
for delete
using (public.current_role_name() = 'admin');

drop policy if exists "users self or admin" on public.users;
create policy "users self or admin" on public.users
for select
using (public.current_role_name() = 'admin' or id = auth.uid());

drop policy if exists "users admin update" on public.users;
create policy "users admin update" on public.users
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');

drop policy if exists "orders select by role" on public.orders;
create policy "orders select by role" on public.orders
for select
using (
  public.current_role_name() = 'admin'
  or public.current_role_name() = 'production_lead'
  or (public.current_role_name() = 'manager' and manager_id = auth.uid())
  or (public.current_role_name() = 'driver' and assigned_driver_id = auth.uid())
  or (
    public.current_role_name() = 'logistician'
    and (
      logistician_id = auth.uid()
      or exists (
        select 1
        from public.order_logisticians ol
        where ol.order_id = orders.id and ol.logistician_id = auth.uid()
      )
    )
  )
);

drop policy if exists "orders insert managers admin" on public.orders;
create policy "orders insert managers admin" on public.orders
for insert
with check (public.current_role_name() in ('manager', 'admin'));

drop policy if exists "orders update by workflow role" on public.orders;
create policy "orders update by workflow role" on public.orders
for update
using (
  public.current_role_name() = 'admin'
  or (public.current_role_name() = 'manager' and manager_id = auth.uid())
  or (public.current_role_name() = 'driver' and assigned_driver_id = auth.uid())
  or public.current_role_name() = 'production_lead'
  or (
    public.current_role_name() = 'logistician'
    and (
      logistician_id = auth.uid()
      or exists (
        select 1
        from public.order_logisticians ol
        where ol.order_id = orders.id and ol.logistician_id = auth.uid()
      )
    )
  )
)
with check (
  public.current_role_name() = 'admin'
  or (public.current_role_name() = 'manager' and manager_id = auth.uid())
  or (public.current_role_name() = 'driver' and assigned_driver_id = auth.uid())
  or public.current_role_name() = 'production_lead'
  or (
    public.current_role_name() = 'logistician'
    and (
      logistician_id = auth.uid()
      or exists (
        select 1
        from public.order_logisticians ol
        where ol.order_id = orders.id and ol.logistician_id = auth.uid()
      )
    )
  )
);

drop policy if exists "history select by order role" on public.order_history;
create policy "history select by order role" on public.order_history
for select
using (
  exists (
    select 1
    from public.orders o
    where o.id = order_history.order_id
      and (
        public.current_role_name() = 'admin'
        or public.current_role_name() = 'production_lead'
        or (public.current_role_name() = 'manager' and o.manager_id = auth.uid())
        or (public.current_role_name() = 'driver' and o.assigned_driver_id = auth.uid())
        or (
          public.current_role_name() = 'logistician'
          and (
            o.logistician_id = auth.uid()
            or exists (
              select 1
              from public.order_logisticians ol
              where ol.order_id = o.id and ol.logistician_id = auth.uid()
            )
          )
        )
      )
  )
);

drop policy if exists "history insert workflow" on public.order_history;
create policy "history insert workflow" on public.order_history
for insert
with check (public.current_role_name() in ('manager', 'production_lead', 'logistician', 'driver', 'admin'));

drop policy if exists "order groups select by role" on public.order_groups;
create policy "order groups select by role" on public.order_groups
for select
using (true);

drop policy if exists "order groups update coordination roles" on public.order_groups;
create policy "order groups update coordination roles" on public.order_groups
for update
using (public.current_role_name() in ('manager', 'logistician', 'admin'))
with check (public.current_role_name() in ('manager', 'logistician', 'admin'));

drop policy if exists "order groups insert service roles" on public.order_groups;
create policy "order groups insert service roles" on public.order_groups
for insert
with check (public.current_role_name() in ('manager', 'logistician', 'admin'));

drop policy if exists "slots by order role" on public.delivery_slots;
create policy "slots by order role" on public.delivery_slots
for all
using (
  exists (
    select 1
    from public.orders o
    where o.id = delivery_slots.order_id
      and (
        public.current_role_name() = 'admin'
        or public.current_role_name() = 'production_lead'
        or (public.current_role_name() = 'manager' and o.manager_id = auth.uid())
        or (public.current_role_name() = 'driver' and o.assigned_driver_id = auth.uid())
        or (
          public.current_role_name() = 'logistician'
          and (
            o.logistician_id = auth.uid()
            or exists (
              select 1
              from public.order_logisticians ol
              where ol.order_id = o.id and ol.logistician_id = auth.uid()
            )
          )
        )
      )
  )
)
with check (public.current_role_name() in ('logistician', 'admin'));

drop policy if exists "chat by order role" on public.chat_messages;
create policy "chat by order role" on public.chat_messages
for select
using (
  exists (
    select 1
    from public.orders o
    where o.id = chat_messages.order_id
      and (
        public.current_role_name() = 'admin'
        or public.current_role_name() = 'production_lead'
        or (public.current_role_name() = 'manager' and o.manager_id = auth.uid())
        or (public.current_role_name() = 'driver' and o.assigned_driver_id = auth.uid())
        or (
          public.current_role_name() = 'logistician'
          and (
            o.logistician_id = auth.uid()
            or exists (
              select 1
              from public.order_logisticians ol
              where ol.order_id = o.id and ol.logistician_id = auth.uid()
            )
          )
        )
      )
  )
);

drop policy if exists "chat insert workflow" on public.chat_messages;
create policy "chat insert workflow" on public.chat_messages
for insert
with check (public.current_role_name() in ('manager', 'logistician', 'admin'));

drop policy if exists "order logisticians by role" on public.order_logisticians;
create policy "order logisticians by role" on public.order_logisticians
for all
using (public.current_role_name() in ('logistician', 'admin'))
with check (public.current_role_name() in ('logistician', 'admin'));

drop policy if exists "error logs admin only" on public.error_logs;
create policy "error logs admin only" on public.error_logs
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');

drop policy if exists "delivery invitations admin only" on public.delivery_invitations;
create policy "delivery invitations admin only" on public.delivery_invitations
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');

alter table public.rate_limits enable row level security;
drop policy if exists "rate limits admin only" on public.rate_limits;
create policy "rate limits admin only" on public.rate_limits
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');

drop policy if exists "integration events admin only" on public.integration_events;
create policy "integration events admin only" on public.integration_events
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');
