create extension if not exists pgcrypto;

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),
  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',
  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())
);

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.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'));

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();

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_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_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, ''))
);

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;

drop policy if exists "roles admin only" on public.roles;
create policy "roles admin only" on public.roles
for all
using (public.current_role_name() = 'admin')
with check (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 (id = auth.uid() or public.current_role_name() = 'admin');

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 "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');
