/*
 * -------------------------------------------------------
 * Module Engine Schema
 * Phase 2: modules, module_fields, module_permissions,
 *          module_relations, module_query() RPC
 * -------------------------------------------------------
 */

-- =====================================================
-- 1. modules — dynamic module definitions
-- =====================================================
create table if not exists public.modules (
  id uuid primary key default gen_random_uuid(),
  account_id uuid not null references public.accounts(id) on delete cascade,
  name text not null,
  display_name text not null,
  description text,
  icon text default 'table',
  table_name text,
  primary_key_column text default 'id',
  status public.cms_module_status not null default 'active',
  sort_order integer not null default 0,

  -- Defaults
  default_sort_field text,
  default_sort_direction text default 'asc' check (default_sort_direction in ('asc', 'desc')),
  default_page_size integer default 25,

  -- Feature toggles
  enable_search boolean not null default true,
  enable_filter boolean not null default true,
  enable_export boolean not null default true,
  enable_import boolean not null default false,
  enable_print boolean not null default true,
  enable_copy boolean not null default false,
  enable_bulk_edit boolean not null default false,
  enable_history boolean not null default true,
  enable_soft_delete boolean not null default true,
  enable_lock boolean not null default false,

  -- Extensibility
  hooks jsonb not null default '{}'::jsonb,

  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),

  unique(account_id, name)
);

comment on table public.modules is 'Dynamic module definitions — replaces legacy m_module';

-- Indexes
create index ix_modules_account on public.modules(account_id);
create index ix_modules_status on public.modules(account_id, status);

-- RLS
alter table public.modules enable row level security;
revoke all on public.modules from authenticated, service_role;
grant select, insert, update, delete on public.modules to authenticated;
grant all on public.modules to service_role;

create policy modules_select on public.modules for select to authenticated
  using (public.has_role_on_account(account_id));

create policy modules_insert on public.modules for insert to authenticated
  with check (
    public.has_permission(auth.uid(), account_id, 'modules.manage'::public.app_permissions)
  );

create policy modules_update on public.modules for update to authenticated
  using (
    public.has_permission(auth.uid(), account_id, 'modules.manage'::public.app_permissions)
  );

create policy modules_delete on public.modules for delete to authenticated
  using (
    public.has_permission(auth.uid(), account_id, 'modules.manage'::public.app_permissions)
  );

-- Auto-update timestamp
create trigger trg_modules_updated_at
  before update on public.modules
  for each row execute function public.update_account_settings_timestamp();

-- =====================================================
-- 2. module_fields — field definitions per module
-- =====================================================
create table if not exists public.module_fields (
  id uuid primary key default gen_random_uuid(),
  module_id uuid not null references public.modules(id) on delete cascade,
  name text not null,
  display_name text not null,
  field_type public.cms_field_type not null default 'text',
  sql_type text default 'text',

  -- Constraints
  is_required boolean not null default false,
  is_unique boolean not null default false,
  default_value text,
  min_value numeric,
  max_value numeric,
  max_length integer,
  regex_pattern text,

  -- Layout
  sort_order integer not null default 0,
  width text default 'full',
  section text default 'default',
  row_index integer default 0,
  col_index integer default 0,
  placeholder text,
  help_text text,

  -- Visibility
  show_in_table boolean not null default true,
  show_in_form boolean not null default true,
  show_in_search boolean not null default false,
  show_in_filter boolean not null default false,
  show_in_export boolean not null default true,
  show_in_print boolean not null default true,

  -- Behavior
  is_sortable boolean not null default true,
  is_readonly boolean not null default false,
  is_encrypted boolean not null default false,
  is_copyable boolean not null default true,
  validation_fn text,

  -- Lookup (foreign key to another module)
  lookup_module_id uuid references public.modules(id) on delete set null,
  lookup_display_field text,
  lookup_value_field text,

  -- Select options (for select/radio/checkbox fields)
  select_options jsonb,

  -- GDPR
  is_personal_data boolean not null default false,
  gdpr_purpose text,

  -- File upload config
  allowed_mime_types text[],
  max_file_size bigint,

  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),

  unique(module_id, name)
);

comment on table public.module_fields is 'Field definitions per module — replaces legacy m_modulfeld';

-- Indexes
create index ix_module_fields_module on public.module_fields(module_id);
create index ix_module_fields_sort on public.module_fields(module_id, sort_order);
create index ix_module_fields_lookup on public.module_fields(lookup_module_id);

-- RLS — inherits from parent module's account
alter table public.module_fields enable row level security;
revoke all on public.module_fields from authenticated, service_role;
grant select, insert, update, delete on public.module_fields to authenticated;
grant all on public.module_fields to service_role;

create policy module_fields_select on public.module_fields for select to authenticated
  using (
    exists (
      select 1 from public.modules m
      where m.id = module_fields.module_id
      and public.has_role_on_account(m.account_id)
    )
  );

create policy module_fields_insert on public.module_fields for insert to authenticated
  with check (
    exists (
      select 1 from public.modules m
      where m.id = module_fields.module_id
      and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
    )
  );

create policy module_fields_update on public.module_fields for update to authenticated
  using (
    exists (
      select 1 from public.modules m
      where m.id = module_fields.module_id
      and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
    )
  );

create policy module_fields_delete on public.module_fields for delete to authenticated
  using (
    exists (
      select 1 from public.modules m
      where m.id = module_fields.module_id
      and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
    )
  );

create trigger trg_module_fields_updated_at
  before update on public.module_fields
  for each row execute function public.update_account_settings_timestamp();

-- =====================================================
-- 3. module_permissions — per-module role permissions
-- =====================================================
create table if not exists public.module_permissions (
  id uuid primary key default gen_random_uuid(),
  module_id uuid not null references public.modules(id) on delete cascade,
  role varchar(50) not null references public.roles(name),
  can_read boolean not null default true,
  can_insert boolean not null default false,
  can_update boolean not null default false,
  can_delete boolean not null default false,
  can_lock boolean not null default false,
  can_import boolean not null default false,
  can_export boolean not null default true,
  can_print boolean not null default true,
  can_bulk_edit boolean not null default false,
  can_manage boolean not null default false,

  unique(module_id, role)
);

comment on table public.module_permissions is 'Per-module, per-role permission overrides';

create index ix_module_permissions_module on public.module_permissions(module_id);

alter table public.module_permissions enable row level security;
revoke all on public.module_permissions from authenticated, service_role;
grant select, insert, update, delete on public.module_permissions to authenticated;
grant all on public.module_permissions to service_role;

create policy module_permissions_select on public.module_permissions for select to authenticated
  using (
    exists (
      select 1 from public.modules m
      where m.id = module_permissions.module_id
      and public.has_role_on_account(m.account_id)
    )
  );

create policy module_permissions_mutate on public.module_permissions for all to authenticated
  using (
    exists (
      select 1 from public.modules m
      where m.id = module_permissions.module_id
      and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
    )
  );

-- =====================================================
-- 4. module_relations — inter-module relationships
-- =====================================================
create table if not exists public.module_relations (
  id uuid primary key default gen_random_uuid(),
  source_module_id uuid not null references public.modules(id) on delete cascade,
  source_field_id uuid not null references public.module_fields(id) on delete cascade,
  target_module_id uuid not null references public.modules(id) on delete cascade,
  target_field_id uuid references public.module_fields(id) on delete set null,
  relation_type text not null check (relation_type in ('lookup', 'parent', 'child', 'many_to_many')),
  created_at timestamptz not null default now()
);

comment on table public.module_relations is 'Relationships between modules (lookup, parent-child, M:N)';

create index ix_module_relations_source on public.module_relations(source_module_id);
create index ix_module_relations_target on public.module_relations(target_module_id);

alter table public.module_relations enable row level security;
revoke all on public.module_relations from authenticated, service_role;
grant select, insert, update, delete on public.module_relations to authenticated;
grant all on public.module_relations to service_role;

create policy module_relations_select on public.module_relations for select to authenticated
  using (
    exists (
      select 1 from public.modules m
      where m.id = module_relations.source_module_id
      and public.has_role_on_account(m.account_id)
    )
  );

create policy module_relations_mutate on public.module_relations for all to authenticated
  using (
    exists (
      select 1 from public.modules m
      where m.id = module_relations.source_module_id
      and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
    )
  );

-- =====================================================
-- 5. module_records — generic record storage
-- =====================================================
create table if not exists public.module_records (
  id uuid primary key default gen_random_uuid(),
  module_id uuid not null references public.modules(id) on delete cascade,
  account_id uuid not null references public.accounts(id) on delete cascade,
  data jsonb not null default '{}'::jsonb,
  status public.cms_record_status not null default 'active',
  locked_by uuid references auth.users(id) on delete set null,
  locked_at timestamptz,
  created_by uuid references auth.users(id) on delete set null,
  updated_by uuid references auth.users(id) on delete set null,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

comment on table public.module_records is 'Generic record storage for all dynamic modules';

create index ix_module_records_module on public.module_records(module_id);
create index ix_module_records_account on public.module_records(account_id);
create index ix_module_records_status on public.module_records(module_id, status);
create index ix_module_records_data on public.module_records using gin(data);
create index ix_module_records_created on public.module_records(created_at desc);

alter table public.module_records enable row level security;
revoke all on public.module_records from authenticated, service_role;
grant select, insert, update, delete on public.module_records to authenticated;
grant all on public.module_records to service_role;

create policy module_records_select on public.module_records for select to authenticated
  using (public.has_role_on_account(account_id));

create policy module_records_insert on public.module_records for insert to authenticated
  with check (
    public.has_permission(auth.uid(), account_id, 'modules.insert'::public.app_permissions)
  );

create policy module_records_update on public.module_records for update to authenticated
  using (
    public.has_permission(auth.uid(), account_id, 'modules.write'::public.app_permissions)
  );

create policy module_records_delete on public.module_records for delete to authenticated
  using (
    public.has_permission(auth.uid(), account_id, 'modules.delete'::public.app_permissions)
  );

create trigger trg_module_records_updated_at
  before update on public.module_records
  for each row execute function public.update_account_settings_timestamp();

-- =====================================================
-- 6. module_query() — dynamic query RPC
-- =====================================================
create or replace function public.module_query(
  p_module_id uuid,
  p_filters jsonb default '[]'::jsonb,
  p_sort_field text default null,
  p_sort_direction text default 'asc',
  p_page integer default 1,
  p_page_size integer default 25,
  p_search text default null
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_account_id uuid;
  v_total bigint;
  v_offset integer;
  v_records jsonb;
  v_query text;
  v_where text := '';
  v_filter jsonb;
  v_field text;
  v_operator text;
  v_value text;
begin
  -- Get the module's account_id and verify access
  select m.account_id into v_account_id
  from public.modules m
  where m.id = p_module_id and m.status = 'active';

  if v_account_id is null then
    raise exception 'Module not found or inactive';
  end if;

  if not public.has_role_on_account(v_account_id) then
    raise exception 'Access denied';
  end if;

  -- Build filter WHERE clause from JSON filters
  -- Each filter: {"field": "name", "operator": "eq", "value": "test"}
  if p_filters is not null and jsonb_array_length(p_filters) > 0 then
    for v_filter in select * from jsonb_array_elements(p_filters)
    loop
      v_field := v_filter->>'field';
      v_operator := v_filter->>'operator';
      v_value := v_filter->>'value';

      -- Sanitize field name (only alphanumeric + underscore)
      if v_field !~ '^[a-zA-Z_][a-zA-Z0-9_]*$' then
        continue;
      end if;

      case v_operator
        when 'eq' then
          v_where := v_where || format(' AND data->>%L = %L', v_field, v_value);
        when 'neq' then
          v_where := v_where || format(' AND data->>%L != %L', v_field, v_value);
        when 'gt' then
          v_where := v_where || format(' AND (data->>%L)::numeric > %L::numeric', v_field, v_value);
        when 'gte' then
          v_where := v_where || format(' AND (data->>%L)::numeric >= %L::numeric', v_field, v_value);
        when 'lt' then
          v_where := v_where || format(' AND (data->>%L)::numeric < %L::numeric', v_field, v_value);
        when 'lte' then
          v_where := v_where || format(' AND (data->>%L)::numeric <= %L::numeric', v_field, v_value);
        when 'like' then
          v_where := v_where || format(' AND data->>%L ILIKE %L', v_field, '%' || v_value || '%');
        when 'is_null' then
          v_where := v_where || format(' AND (data->>%L IS NULL OR data->>%L = '''')', v_field, v_field);
        when 'not_null' then
          v_where := v_where || format(' AND data->>%L IS NOT NULL AND data->>%L != ''''', v_field, v_field);
        else
          null; -- skip unknown operators
      end case;
    end loop;
  end if;

  -- Text search across all data fields
  if p_search is not null and p_search != '' then
    v_where := v_where || format(' AND data::text ILIKE %L', '%' || p_search || '%');
  end if;

  -- Count total
  execute format(
    'SELECT count(*) FROM public.module_records WHERE module_id = %L AND status != ''deleted'' %s',
    p_module_id, v_where
  ) into v_total;

  -- Calculate offset
  v_offset := (p_page - 1) * p_page_size;

  -- Build sort
  if p_sort_field is not null and p_sort_field ~ '^[a-zA-Z_][a-zA-Z0-9_]*$' then
    v_query := format(
      'SELECT jsonb_agg(row_to_json(r)) FROM (
        SELECT id, data, status, locked_by, locked_at, created_by, updated_by, created_at, updated_at
        FROM public.module_records
        WHERE module_id = %L AND status != ''deleted'' %s
        ORDER BY data->>%L %s NULLS LAST
        LIMIT %L OFFSET %L
      ) r',
      p_module_id, v_where, p_sort_field,
      case when p_sort_direction = 'desc' then 'DESC' else 'ASC' end,
      p_page_size, v_offset
    );
  else
    v_query := format(
      'SELECT jsonb_agg(row_to_json(r)) FROM (
        SELECT id, data, status, locked_by, locked_at, created_by, updated_by, created_at, updated_at
        FROM public.module_records
        WHERE module_id = %L AND status != ''deleted'' %s
        ORDER BY created_at DESC
        LIMIT %L OFFSET %L
      ) r',
      p_module_id, v_where, p_page_size, v_offset
    );
  end if;

  execute v_query into v_records;

  return jsonb_build_object(
    'data', coalesce(v_records, '[]'::jsonb),
    'pagination', jsonb_build_object(
      'page', p_page,
      'pageSize', p_page_size,
      'total', v_total,
      'totalPages', ceil(v_total::numeric / p_page_size::numeric)::integer
    )
  );
end;
$$;

grant execute on function public.module_query(uuid, jsonb, text, text, integer, integer, text)
  to authenticated, service_role;
