-- ════════════════════════════════════════════════════════════════════════════
-- v3.upd_form3_cell / v3.upd_form3_cells — точечная правка ячейки FORM_3.
-- Хранение FORM_3 идёт через rf_project_report → rf_project_report_line →
-- rf_project_report_quarter (PK = (rf_project_report_line_id, quarter)).
-- API симметричен upd_form_cell, но идентификатор «строки» — это
-- rf_project_report_line.id (то же `line_id`, что в JSONB read-функции).
--
-- Поддерживаемые ключи p_column (см. v_form3_report_jsonb.sql):
--   Факт:        'qN.adj_by_items', 'qN.adj_increase', 'qN.m1', 'qN.m2', 'qN.m3'
--                (+ 'q4.spod' — только IV квартал)
--   Базовый план:   'q1.base_plan' … 'q4.base_plan'      (все 4 квартала)
--   Скоррект план:  'q2.corrected_plan' … 'q4.corrected_plan'
--                   (без q1 — шаблон рисует скорр.план с II квартала)
--
-- Computed (RO): qN.total_corr, qN.quarter_actual, qN.economy (=«Остаток»),
-- qN.carryover, base_plan.year / totals.* (годовые итоги считаются на лету).
--
-- Атрибуты rf_project_report (dev_type, vsp_format, object_address, ...) —
-- НЕ через эту функцию (это header отчёта, не ячейка строки).
-- ════════════════════════════════════════════════════════════════════════════


DROP FUNCTION IF EXISTS v3._apply_form3_cell(INT, TEXT, JSONB);

CREATE FUNCTION v3._apply_form3_cell(
    p_line_id INT,                  -- rf_project_report_line.id
    p_column  TEXT,
    p_value   JSONB
)
RETURNS VOID
AS $function$
DECLARE
  v_parts       TEXT[];
  v_scope       TEXT;
  v_field       TEXT;
  v_q           SMALLINT;
  v_target_col  TEXT;
  v_target_type TEXT;
  v_sql         TEXT;
BEGIN
  v_parts := string_to_array(p_column, '.');
  IF array_length(v_parts, 1) < 2 THEN
    RAISE EXCEPTION 'bad_column_format: %, expected scope.field', p_column;
  END IF;
  v_scope := v_parts[1];
  v_field := v_parts[2];

  IF v_scope = 'totals' OR v_field IN ('total_corr','quarter_actual','economy','carryover') THEN
    RAISE EXCEPTION 'computed_field: %', p_column;
  END IF;

  IF v_scope NOT IN ('q1','q2','q3','q4') THEN
    RAISE EXCEPTION 'unknown_scope: % (FORM_3 supports only q1..q4)', v_scope;
  END IF;

  v_q := substring(v_scope FROM 2)::SMALLINT;

  -- Маппинг JSONB-ключ → колонка rf_project_report_quarter
  CASE v_field
    WHEN 'adj_by_items' THEN v_target_col := 'adj_by_items'; v_target_type := 'NUMERIC';
    WHEN 'adj_increase' THEN v_target_col := 'adj_increase'; v_target_type := 'NUMERIC';
    WHEN 'm1'           THEN v_target_col := 'actual_m1';    v_target_type := 'NUMERIC';
    WHEN 'm2'           THEN v_target_col := 'actual_m2';    v_target_type := 'NUMERIC';
    WHEN 'm3'           THEN v_target_col := 'actual_m3';    v_target_type := 'NUMERIC';
    WHEN 'spod' THEN
      IF v_q <> 4 THEN RAISE EXCEPTION 'spod is q4-only: %', p_column; END IF;
      v_target_col := 'actual_spod'; v_target_type := 'NUMERIC';
    WHEN 'base_plan'      THEN v_target_col := 'base_plan';      v_target_type := 'NUMERIC';
    WHEN 'corrected_plan' THEN
      IF v_q = 1 THEN RAISE EXCEPTION 'corrected_plan is q2-q4 only: %', p_column; END IF;
      v_target_col := 'corrected_plan'; v_target_type := 'NUMERIC';
    ELSE
      RAISE EXCEPTION 'unknown_column: %', p_column;
  END CASE;

  v_sql := format(
    'INSERT INTO v3.rf_project_report_quarter (rf_project_report_line_id, quarter, %1$I) '
    || 'VALUES ($1, $2, ($3 #>> ''{}'')::%2$s) '
    || 'ON CONFLICT (rf_project_report_line_id, quarter) DO UPDATE SET %1$I = EXCLUDED.%1$I',
    v_target_col, v_target_type
  );
  EXECUTE v_sql USING p_line_id, v_q, p_value;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;


-- ─── Public batch ──────────────────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.upd_form3_cells(INT, JSONB);

CREATE FUNCTION v3.upd_form3_cells(
    p_report_id INT,
    p_changes   JSONB
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
  v_change         JSONB;
  v_line_id        INT;
  v_column         TEXT;
  v_value          JSONB;
  v_changed_lines  INT[] := ARRAY[]::INT[];
BEGIN
  IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
    RAISE EXCEPTION 'p_changes must be a JSONB array';
  END IF;

  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;

  FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
  LOOP
    v_line_id := (v_change->>'line_id')::INT;
    v_column  := v_change->>'column';
    v_value   := v_change->'value';

    IF v_line_id IS NULL OR v_column IS NULL THEN
      RAISE EXCEPTION 'change must have line_id and column: %', v_change;
    END IF;

    IF NOT EXISTS (
      SELECT 1 FROM v3.rf_project_report_line
       WHERE id = v_line_id AND rf_project_report_id = p_report_id
    ) THEN
      RAISE EXCEPTION 'rf_project_report_line id=% не принадлежит report id=%',
        v_line_id, p_report_id;
    END IF;

    PERFORM v3._apply_form3_cell(v_line_id, v_column, v_value);

    IF NOT (v_line_id = ANY(v_changed_lines)) THEN
      v_changed_lines := v_changed_lines || v_line_id;
    END IF;
  END LOOP;

  -- Возврат: иерархия по пути изменённых rfprl + изменённые INPUT по line_id
  DECLARE
    v_eid_set INT[];
    v_ei_path INT[];
  BEGIN
    SELECT array_agg(DISTINCT rfprl.expense_item_id)
      INTO v_eid_set
      FROM v3.rf_project_report_line rfprl
     WHERE rfprl.id = ANY(v_changed_lines);
    v_ei_path := v3._expense_item_path_set(v_eid_set);

    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form3_report_jsonb(p_report_id, NULL) v
     WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
            AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
        OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(v_changed_lines))
     ORDER BY v.sort_order;
  END;
END;
$function$
LANGUAGE plpgsql VOLATILE;

-- ─── Public single-cell wrapper ────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.upd_form3_cell(INT, INT, TEXT, JSONB);

CREATE FUNCTION v3.upd_form3_cell(
    p_report_id INT,
    p_line_id   INT,
    p_column    TEXT,
    p_value     JSONB
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
  SELECT * FROM v3.upd_form3_cells(
    p_report_id,
    jsonb_build_array(jsonb_build_object(
      'line_id', p_line_id,
      'column',  p_column,
      'value',   p_value
    ))
  );
$function$ 
LANGUAGE sql VOLATILE;
-- ════════════════════════════════════════════════════════════════════════════
-- FORM_3 write API: project header + line ADD/DEL + project ADD-master
-- ════════════════════════════════════════════════════════════════════════════
-- Дополняет существующий v3.upd_form3_cell (правка ячеек квартальных
-- значений в rf_project_report_quarter) — здесь функции для:
--   1) v3.upd_project              — правка атрибутов карточки проекта
--   2) v3.add_form3_line           — [DEPRECATED в UI] добавить позицию вручную.
--   3) v3.del_form3_line           — [DEPRECATED в UI] удалить позицию.
--      В FORM_3 сетка статей фиксирована (нет макроса добавления строк) —
--      строки засеваются автоматически, юзер пишет прямо в строку статьи.
--      add/del_form3_line оставлены для админ-правок/legacy.
--   3b) v3.seed_form3_report_lines — засев полного набора листовых статей FORM_3.
--   3c) v3.add_project_year        — динамическое дорастание проекта годом
--                                    (2 отчёта + snapshot фаз + сетка статей).
--   4) v3.add_project              — мастер: project + первый год (add_project_year).
-- ════════════════════════════════════════════════════════════════════════════

-- ─── 1. upd_project ────────────────────────────────────────────────────────

DROP FUNCTION IF EXISTS v3.upd_project(INT, TEXT, JSONB);

CREATE FUNCTION v3.upd_project(
    p_project_id INT,
    p_column     TEXT,
    p_value      JSONB
)
RETURNS JSONB    -- весь header проекта после правки
AS $function$
DECLARE
  v_target_col  TEXT;
  v_target_type TEXT;
  v_sql         TEXT;
  v_out         JSONB;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
    RAISE EXCEPTION 'project id=% не существует', p_project_id;
  END IF;

  -- Editable атрибуты (CHECK-ы из migrate_form3_v1.sql применятся при UPDATE).
  -- Не editable: level (структура), parent_id (иерархия).
  CASE p_column
    WHEN 'name'           THEN v_target_col := 'name';           v_target_type := 'TEXT';
    WHEN 'project_type'   THEN v_target_col := 'project_type';   v_target_type := 'TEXT';
    WHEN 'vsp_format'     THEN v_target_col := 'vsp_format';     v_target_type := 'TEXT';
    WHEN 'placement_type' THEN v_target_col := 'placement_type'; v_target_type := 'TEXT';
    WHEN 'object_address' THEN v_target_col := 'object_address'; v_target_type := 'TEXT';
    WHEN 'system_code'    THEN v_target_col := 'system_code';    v_target_type := 'TEXT';
    WHEN 'staff_count'    THEN v_target_col := 'staff_count';    v_target_type := 'INTEGER';
    WHEN 'total_area'     THEN v_target_col := 'total_area';     v_target_type := 'NUMERIC';
    WHEN 'org_unit_id'    THEN v_target_col := 'org_unit_id';    v_target_type := 'INTEGER';
    WHEN 'level','parent_id' THEN
      RAISE EXCEPTION 'structural_field: % (правится отдельно через ADD project / структурный API)', p_column;
    WHEN 'id' THEN
      RAISE EXCEPTION 'key_field: id (RO)';
    ELSE
      RAISE EXCEPTION 'unknown_column: %', p_column;
  END CASE;

  -- FK санити (если применимо)
  IF v_target_col = 'org_unit_id' AND p_value <> 'null'::jsonb THEN
    IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = (p_value #>> '{}')::INT) THEN
      RAISE EXCEPTION 'org_unit id=% не существует', (p_value #>> '{}');
    END IF;
  END IF;

  v_sql := format(
    'UPDATE v3.project SET %1$I = ($2 #>> ''{}'')::%2$s WHERE id = $1',
    v_target_col, v_target_type
  );
  EXECUTE v_sql USING p_project_id, p_value;

  -- Возврат — весь header проекта
  SELECT jsonb_build_object(
           'id',             p.id,
           'name',           p.name,
           'level',          p.level,
           'parent_id',      p.parent_id,
           'org_unit_id',    p.org_unit_id,
           'project_type',   p.project_type,
           'vsp_format',     p.vsp_format,
           'placement_type', p.placement_type,
           'object_address', p.object_address,
           'system_code',    p.system_code,
           'staff_count',    p.staff_count,
           'total_area',     p.total_area
         )
    INTO v_out
    FROM v3.project p WHERE p.id = p_project_id;
  RETURN v_out;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── 2. add_form3_line ─────────────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.add_form3_line(INT, INT);

CREATE FUNCTION v3.add_form3_line(
    p_report_id        INT,
    p_expense_item_id  INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
  v_new_id INT;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.expense_item WHERE id = p_expense_item_id) THEN
    RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.expense_item_form_type
                  WHERE expense_item_id = p_expense_item_id AND form_type_code = 'FORM_3') THEN
    RAISE EXCEPTION 'expense_item id=% не привязан к form_type FORM_3', p_expense_item_id;
  END IF;

  INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id)
  VALUES (p_report_id, p_expense_item_id)
  RETURNING id INTO v_new_id;

  -- Аудит: ROW_CREATE
  PERFORM v3.log_event(
    'ROW_CREATE', 'ROW_CREATE',
    jsonb_build_object(
      'sheet',           'FORM_3',
      'line_id',         v_new_id,
      'report_id',       p_report_id,
      'expense_item_id', p_expense_item_id
    )
  );

  -- Возврат: только иерархия по пути нового ei + новая INPUT
  DECLARE v_ei_path INT[];
  BEGIN
    v_ei_path := v3._expense_item_path_set(ARRAY[p_expense_item_id]);
    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form3_report_jsonb(p_report_id, NULL) v
     WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
            AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
        OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
     ORDER BY v.sort_order;
  END;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── 3. del_form3_line ─────────────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.del_form3_line(INT);

CREATE FUNCTION v3.del_form3_line(
    p_line_id INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
  v_report_id INT;
  v_eid       INT;
  v_ei_path   INT[];
BEGIN
  -- Извлекаем report_id + ei ДО DELETE (нужен для path)
  SELECT rfprl.rf_project_report_id, rfprl.expense_item_id
    INTO v_report_id, v_eid
    FROM v3.rf_project_report_line rfprl
   WHERE rfprl.id = p_line_id;
  IF v_report_id IS NULL THEN
    RAISE EXCEPTION 'rf_project_report_line id=% не существует', p_line_id;
  END IF;

  v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);

  -- Каскад (FK без ON DELETE CASCADE)
  DELETE FROM v3.rf_project_report_quarter WHERE rf_project_report_line_id = p_line_id;
  DELETE FROM v3.rf_project_report_line    WHERE id = p_line_id;

  -- Аудит: ROW_DELETE
  PERFORM v3.log_event(
    'ROW_DELETE', 'ROW_DELETE',
    jsonb_build_object(
      'sheet',           'FORM_3',
      'line_id',         p_line_id,
      'report_id',       v_report_id,
      'expense_item_id', v_eid
    )
  );

  -- Возврат: только иерархия по пути удалённой (INPUT уже нет)
  RETURN QUERY
  SELECT v.row_type, v.depth, v.sort_order, v.data
    FROM v3.v_form3_report_jsonb(v_report_id, NULL) v
   WHERE v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
     AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
   ORDER BY v.sort_order;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── 3b. seed_form3_report_lines (фиксированная сетка статей) ──────────────
-- В FORM_3 строки НЕ добавляются пользователем (в шаблоне нет макроса добавления
-- строк) — сетка статей фиксирована, юзер пишет прямо в строку статьи. Поэтому
-- при создании отчёта засеваем ПОЛНЫЙ набор листовых статей FORM_3 (листовая =
-- FORM_3-статья без FORM_3-детей). Идемпотентно (uq_v3_rf_project_report_line).
-- Возвращает число вставленных строк.
DROP FUNCTION IF EXISTS v3.seed_form3_report_lines(INT);

CREATE FUNCTION v3.seed_form3_report_lines(p_report_id INT)
RETURNS INT
AS $function$
DECLARE
  v_cnt INT;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;

  INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id)
  SELECT p_report_id, e.id
    FROM v3.expense_item e
    JOIN v3.expense_item_form_type eft
      ON eft.expense_item_id = e.id AND eft.form_type_code = 'FORM_3'
   WHERE NOT EXISTS (
           SELECT 1 FROM v3.expense_item c
            JOIN v3.expense_item_form_type eftc
              ON eftc.expense_item_id = c.id AND eftc.form_type_code = 'FORM_3'
           WHERE c.parent_id = e.id
         )
  ON CONFLICT (rf_project_report_id, expense_item_id) DO NOTHING;

  GET DIAGNOSTICS v_cnt = ROW_COUNT;
  RETURN v_cnt;
END;
$function$
LANGUAGE plpgsql VOLATILE;

-- ─── 3c. add_project_year (динамическое дорастание проекта по годам) ───────
-- Проект может дополняться годами в процессе работы. Заводит пару отчётов
-- (LIMIT + CURRENT_EXPENSES) на (project, year), снапшотит RBAC-фазы и засевает
-- фиксированную сетку статей. Год несёт report_id — column_keys без префикса
-- года, RBAC работает пофазно на каждый год-отчёт (см. seed_phase_template).
DROP FUNCTION IF EXISTS v3.add_project_year(INT, INT, BOOLEAN);

CREATE FUNCTION v3.add_project_year(
    p_project_id INT,
    p_year       INT,
    p_emit_audit BOOLEAN DEFAULT true  -- false когда зовётся из add_project (там PROJECT_CREATE)
)
RETURNS TABLE (
    limit_report_id             INT,
    current_expenses_report_id  INT
)
AS $function$
DECLARE
  v_lim INT;
  v_cur INT;
BEGIN
  IF p_year IS NULL THEN
    RAISE EXCEPTION 'p_year обязателен';
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
    RAISE EXCEPTION 'project id=% не существует', p_project_id;
  END IF;
  IF EXISTS (SELECT 1 FROM v3.rf_project_report
              WHERE project_id = p_project_id AND year = p_year) THEN
    RAISE EXCEPTION 'год % у проекта id=% уже заведён', p_year, p_project_id;
  END IF;

  INSERT INTO v3.rf_project_report (project_id, year, report_type)
  VALUES (p_project_id, p_year, 'LIMIT')            RETURNING id INTO v_lim;
  INSERT INTO v3.rf_project_report (project_id, year, report_type)
  VALUES (p_project_id, p_year, 'CURRENT_EXPENSES') RETURNING id INTO v_cur;

  -- Snapshot RBAC-фаз (иначе default deny → отчёт read-only)
  PERFORM v3.copy_template_to_report(v_lim);
  PERFORM v3.copy_template_to_report(v_cur);

  -- Фиксированная сетка статей на оба трека
  PERFORM v3.seed_form3_report_lines(v_lim);
  PERFORM v3.seed_form3_report_lines(v_cur);

  IF p_emit_audit THEN
    PERFORM v3.log_event(
      'PROJECT_YEAR_ADD', 'PROJECT_YEAR_ADD',
      jsonb_build_object(
        'project_id',                 p_project_id,
        'year',                       p_year,
        'limit_report_id',            v_lim,
        'current_expenses_report_id', v_cur
      )
    );
  END IF;

  RETURN QUERY SELECT v_lim, v_cur;
END;
$function$
LANGUAGE plpgsql VOLATILE;

-- ─── 4. add_project (мастер: project + первый год через add_project_year) ──
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, INT,     VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT,          VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT,          VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC, VARCHAR);

CREATE FUNCTION v3.add_project(
    p_name           VARCHAR,
    p_year           INT,
    p_org_unit_id    INT,
    p_level          VARCHAR DEFAULT 'project',  -- 'program' | 'project'
    p_parent_id      INT     DEFAULT NULL,       -- обязателен если level='project' и есть программа
    p_project_type   VARCHAR DEFAULT NULL,
    p_vsp_format     VARCHAR DEFAULT NULL,
    p_placement_type VARCHAR DEFAULT NULL,
    p_object_address VARCHAR DEFAULT NULL,
    p_staff_count    INT     DEFAULT NULL,
    p_total_area     NUMERIC DEFAULT NULL,
    p_system_code    VARCHAR DEFAULT NULL        -- Системный код ВСП
)
RETURNS TABLE (
    project_id                  INT,
    limit_report_id             INT,
    current_expenses_report_id  INT
)
AS $function$
DECLARE
  v_pid INT;
  v_lim INT;
  v_cur INT;
BEGIN
  IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
    RAISE EXCEPTION 'p_name обязателен';
  END IF;
  IF p_year IS NULL THEN
    RAISE EXCEPTION 'p_year обязателен';
  END IF;
  IF p_org_unit_id IS NULL THEN
    RAISE EXCEPTION 'p_org_unit_id обязателен';
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
    RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
  END IF;
  IF p_parent_id IS NOT NULL AND
     NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
    RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
  END IF;

  -- INSERT project (CHECK-ы name/project_type/vsp_format/placement_type
  -- из migrate_form3_v1.sql применятся автоматически).
  INSERT INTO v3.project (
    name, level, parent_id, org_unit_id,
    project_type, vsp_format, placement_type, object_address, staff_count, total_area,
    system_code
  ) VALUES (
    p_name, p_level, p_parent_id, p_org_unit_id,
    p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area,
    p_system_code
  ) RETURNING id INTO v_pid;

  -- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
  -- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие
  -- годы добавляются отдельно через v3.add_project_year.
  SELECT y.limit_report_id, y.current_expenses_report_id
    INTO v_lim, v_cur
    FROM v3.add_project_year(v_pid, p_year, false) y;

  -- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
  PERFORM v3.log_event(
    'PROJECT_CREATE', 'PROJECT_CREATE',
    jsonb_strip_nulls(jsonb_build_object(
      'project_id',     v_pid,
      'name',           p_name,
      'year',           p_year,
      'level',          p_level,
      'parent_id',      p_parent_id,
      'org_unit_id',    p_org_unit_id,
      'project_type',   p_project_type,
      'vsp_format',     p_vsp_format,
      'placement_type', p_placement_type,
      'object_address', p_object_address,
      'system_code',    p_system_code,
      'staff_count',    p_staff_count,
      'total_area',     p_total_area,
      'limit_report_id',            v_lim,
      'current_expenses_report_id', v_cur
    ))
  );

  RETURN QUERY SELECT v_pid, v_lim, v_cur;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.v_form3_report_sections — отображение листа ЛП (LIMIT) или ТР (CURRENT)
-- ════════════════════════════════════════════════════════════════════════════
-- Возвращает иерархию (ROOT/GROUP/ITEM/SUB_ITEM) + INPUT строки в tree-order
-- для одного rf_project_report (один трек: report_type = LIMIT либо
-- CURRENT_EXPENSES). UI выбирает трек, передавая report_id соответствующей
-- записи rf_project_report.
--
-- Колоночная раскладка повторяет шаблон Формы 3:
--   header, q1, q2, q3, q4, year_totals.
--
-- Q1: adj_by_items, adj_increase, total_corr, m1, m2, m3, quarter_actual, economy
-- Q2/Q3: carryover, adj_by_items, adj_increase, total_corr, m1..m3, quarter_actual, economy
-- Q4: carryover, adj_by_items, adj_increase, total_corr, m1..m3, spod, quarter_actual, economy
-- year_totals: total_year_corr, total_year_actual, economy_year
--
-- p_sections — массив включаемых блоков (NULL = все). Допустимые имена:
--   'q1', 'q2', 'q3', 'q4', 'year'
-- ════════════════════════════════════════════════════════════════════════════

DROP FUNCTION IF EXISTS v3.v_form3_report_sections(INT, TEXT[]);

CREATE FUNCTION v3.v_form3_report_sections(
    p_report_id INT,
    p_sections  TEXT[] DEFAULT NULL
)
RETURNS TABLE (
    row_type             VARCHAR,
    depth                INT,
    line_id              INT,
    col_section_code     VARCHAR,
    col_item_id          VARCHAR,
    col_num_group_id     VARCHAR,
    col_name             VARCHAR,
    -- Q1
    col_q1_adj_by_items  NUMERIC,
    col_q1_adj_increase  NUMERIC,
    col_q1_total_corr    NUMERIC,
    col_q1_m1            NUMERIC,
    col_q1_m2            NUMERIC,
    col_q1_m3            NUMERIC,
    col_q1_quarter_actual NUMERIC,
    col_q1_economy       NUMERIC,
    -- Q2
    col_q2_carryover     NUMERIC,
    col_q2_adj_by_items  NUMERIC,
    col_q2_adj_increase  NUMERIC,
    col_q2_total_corr    NUMERIC,
    col_q2_m1            NUMERIC,
    col_q2_m2            NUMERIC,
    col_q2_m3            NUMERIC,
    col_q2_quarter_actual NUMERIC,
    col_q2_economy       NUMERIC,
    -- Q3
    col_q3_carryover     NUMERIC,
    col_q3_adj_by_items  NUMERIC,
    col_q3_adj_increase  NUMERIC,
    col_q3_total_corr    NUMERIC,
    col_q3_m1            NUMERIC,
    col_q3_m2            NUMERIC,
    col_q3_m3            NUMERIC,
    col_q3_quarter_actual NUMERIC,
    col_q3_economy       NUMERIC,
    -- Q4
    col_q4_carryover     NUMERIC,
    col_q4_adj_by_items  NUMERIC,
    col_q4_adj_increase  NUMERIC,
    col_q4_total_corr    NUMERIC,
    col_q4_m1            NUMERIC,
    col_q4_m2            NUMERIC,
    col_q4_m3            NUMERIC,
    col_q4_spod          NUMERIC,
    col_q4_quarter_actual NUMERIC,
    col_q4_economy       NUMERIC,
    -- year totals
    col_year_total_corr   NUMERIC,
    col_year_total_actual NUMERIC,
    col_year_economy      NUMERIC,
    -- план-блоки (v2): Базовый план (q1–q4 + Год) и Скоррект план (q2–q4)
    col_q1_base_plan      NUMERIC,
    col_q2_base_plan      NUMERIC,
    col_q3_base_plan      NUMERIC,
    col_q4_base_plan      NUMERIC,
    col_year_base_plan    NUMERIC,
    col_q2_corrected_plan NUMERIC,
    col_q3_corrected_plan NUMERIC,
    col_q4_corrected_plan NUMERIC,
    _sort_path            INT[]
)
AS $function$
DECLARE
    s_q1   BOOLEAN := p_sections IS NULL OR 'q1'   = ANY(p_sections);
    s_q2   BOOLEAN := p_sections IS NULL OR 'q2'   = ANY(p_sections);
    s_q3   BOOLEAN := p_sections IS NULL OR 'q3'   = ANY(p_sections);
    s_q4   BOOLEAN := p_sections IS NULL OR 'q4'   = ANY(p_sections);
    s_year BOOLEAN := p_sections IS NULL OR 'year' = ANY(p_sections);
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;

  RETURN QUERY
  WITH
    -- Дерево expense_item, ограниченное FORM_3 (через junction)
    tw AS (
      SELECT t.* FROM v3.mv_expense_item_tree t
      JOIN v3.expense_item_form_type eft
        ON eft.expense_item_id = t.id AND eft.form_type_code = 'FORM_3'
    ),
    -- INPUT строки: одна на v3.rf_project_report_line
    page AS (
      SELECT l.id AS lid, l.expense_item_id AS eid,
             t.parent_item_id AS sc,        -- section-код родителя в дереве (1.01.1.)
             ei.item_id AS ic,
             ei.num_group_id AS ng, ei.name AS ename,
             t.path AS tree_path
      FROM v3.rf_project_report_line l
      JOIN v3.expense_item ei ON ei.id = l.expense_item_id
      JOIN tw t ON t.id = l.expense_item_id
      WHERE l.rf_project_report_id = p_report_id
    ),
    -- Поквартальные данные
    jq AS (
      SELECT q.rf_project_report_line_id AS lid, q.quarter,
             q.adj_by_items, q.adj_increase,
             q.actual_m1, q.actual_m2, q.actual_m3, q.actual_spod,
             q.base_plan, q.corrected_plan
      FROM v3.rf_project_report_quarter q
      JOIN v3.rf_project_report_line l ON l.id = q.rf_project_report_line_id
      WHERE l.rf_project_report_id = p_report_id
    ),
    -- INPUT base: соединяем page + 4 квартала
    input_base AS (
      SELECT
        pg.lid, pg.eid, pg.sc, pg.ic, pg.ng, pg.ename, pg.tree_path,
        q1.adj_by_items AS q1_abi, q1.adj_increase AS q1_ai,
        q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3,
        q2.adj_by_items AS q2_abi, q2.adj_increase AS q2_ai,
        q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3,
        q3.adj_by_items AS q3_abi, q3.adj_increase AS q3_ai,
        q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3,
        q4.adj_by_items AS q4_abi, q4.adj_increase AS q4_ai,
        q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3,
        q4.actual_spod AS q4_spod,
        q1.base_plan AS q1_bp, q2.base_plan AS q2_bp,
        q3.base_plan AS q3_bp, q4.base_plan AS q4_bp,
        q2.corrected_plan AS q2_cp, q3.corrected_plan AS q3_cp, q4.corrected_plan AS q4_cp
      FROM page pg
      LEFT JOIN jq q1 ON q1.lid = pg.lid AND q1.quarter = 1
      LEFT JOIN jq q2 ON q2.lid = pg.lid AND q2.quarter = 2
      LEFT JOIN jq q3 ON q3.lid = pg.lid AND q3.quarter = 3
      LEFT JOIN jq q4 ON q4.lid = pg.lid AND q4.quarter = 4
    ),
    -- Computed totals per row
    enriched AS (
      SELECT b.*,
        COALESCE(b.q1_abi,0) + COALESCE(b.q1_ai,0) AS q1_tc,
        COALESCE(b.q1_m1,0) + COALESCE(b.q1_m2,0) + COALESCE(b.q1_m3,0) AS q1_qa,
        COALESCE(b.q2_abi,0) + COALESCE(b.q2_ai,0) AS q2_tc,
        COALESCE(b.q2_m1,0) + COALESCE(b.q2_m2,0) + COALESCE(b.q2_m3,0) AS q2_qa,
        COALESCE(b.q3_abi,0) + COALESCE(b.q3_ai,0) AS q3_tc,
        COALESCE(b.q3_m1,0) + COALESCE(b.q3_m2,0) + COALESCE(b.q3_m3,0) AS q3_qa,
        COALESCE(b.q4_abi,0) + COALESCE(b.q4_ai,0) AS q4_tc,
        COALESCE(b.q4_m1,0) + COALESCE(b.q4_m2,0) + COALESCE(b.q4_m3,0) + COALESCE(b.q4_spod,0) AS q4_qa
      FROM input_base b
    ),
    -- Per-line aggregates for tree rollup
    agg_q AS (
      SELECT l.expense_item_id AS eid, q.quarter,
        SUM(q.adj_by_items) AS abi, SUM(q.adj_increase) AS ai,
        SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3,
        SUM(q.actual_spod) AS spod,
        SUM(q.base_plan) AS bp, SUM(q.corrected_plan) AS cp
      FROM v3.rf_project_report_line l
      JOIN v3.rf_project_report_quarter q ON q.rf_project_report_line_id = l.id
      WHERE l.rf_project_report_id = p_report_id
      GROUP BY l.expense_item_id, q.quarter
    ),
    tw_aq1 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.bp) AS bp
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 1
      GROUP BY tw.id
    ),
    tw_aq2 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.bp) AS bp, SUM(a.cp) AS cp
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 2
      GROUP BY tw.id
    ),
    tw_aq3 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.bp) AS bp, SUM(a.cp) AS cp
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 3
      GROUP BY tw.id
    ),
    tw_aq4 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.spod) AS spod,
             SUM(a.bp) AS bp, SUM(a.cp) AS cp
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 4
      GROUP BY tw.id
    )

  -- ═══ Часть A: INPUT строки ════════════════════════════════════════════════
  SELECT * FROM (
    SELECT
      'INPUT'::VARCHAR AS row_type, 3 AS depth, e.lid::INT AS line_id,
      e.sc, e.ic, e.ng, e.ename,
      -- Q1
      CASE WHEN s_q1 THEN e.q1_abi END,
      CASE WHEN s_q1 THEN e.q1_ai END,
      CASE WHEN s_q1 THEN e.q1_tc END,
      CASE WHEN s_q1 THEN e.q1_m1 END,
      CASE WHEN s_q1 THEN e.q1_m2 END,
      CASE WHEN s_q1 THEN e.q1_m3 END,
      CASE WHEN s_q1 THEN e.q1_qa END,
      CASE WHEN s_q1 THEN ROUND(e.q1_tc - e.q1_qa, 1) END,
      -- Q2 (carryover пока NULL — нет столбца в _quarter; задел на будущее)
      CASE WHEN s_q2 THEN NULL::NUMERIC END,
      CASE WHEN s_q2 THEN e.q2_abi END,
      CASE WHEN s_q2 THEN e.q2_ai END,
      CASE WHEN s_q2 THEN e.q2_tc END,
      CASE WHEN s_q2 THEN e.q2_m1 END,
      CASE WHEN s_q2 THEN e.q2_m2 END,
      CASE WHEN s_q2 THEN e.q2_m3 END,
      CASE WHEN s_q2 THEN e.q2_qa END,
      CASE WHEN s_q2 THEN ROUND(e.q2_tc - e.q2_qa, 1) END,
      -- Q3
      CASE WHEN s_q3 THEN NULL::NUMERIC END,
      CASE WHEN s_q3 THEN e.q3_abi END,
      CASE WHEN s_q3 THEN e.q3_ai END,
      CASE WHEN s_q3 THEN e.q3_tc END,
      CASE WHEN s_q3 THEN e.q3_m1 END,
      CASE WHEN s_q3 THEN e.q3_m2 END,
      CASE WHEN s_q3 THEN e.q3_m3 END,
      CASE WHEN s_q3 THEN e.q3_qa END,
      CASE WHEN s_q3 THEN ROUND(e.q3_tc - e.q3_qa, 1) END,
      -- Q4
      CASE WHEN s_q4 THEN NULL::NUMERIC END,
      CASE WHEN s_q4 THEN e.q4_abi END,
      CASE WHEN s_q4 THEN e.q4_ai END,
      CASE WHEN s_q4 THEN e.q4_tc END,
      CASE WHEN s_q4 THEN e.q4_m1 END,
      CASE WHEN s_q4 THEN e.q4_m2 END,
      CASE WHEN s_q4 THEN e.q4_m3 END,
      CASE WHEN s_q4 THEN e.q4_spod END,
      CASE WHEN s_q4 THEN e.q4_qa END,
      CASE WHEN s_q4 THEN ROUND(e.q4_tc - e.q4_qa, 1) END,
      -- year
      CASE WHEN s_year THEN e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc END,
      CASE WHEN s_year THEN e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa END,
      CASE WHEN s_year THEN ROUND((e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc) -
                                  (e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa), 1) END,
      -- план-блоки
      CASE WHEN s_q1 THEN e.q1_bp END,
      CASE WHEN s_q2 THEN e.q2_bp END,
      CASE WHEN s_q3 THEN e.q3_bp END,
      CASE WHEN s_q4 THEN e.q4_bp END,
      CASE WHEN s_year THEN COALESCE(e.q1_bp,0)+COALESCE(e.q2_bp,0)+
                           COALESCE(e.q3_bp,0)+COALESCE(e.q4_bp,0) END,
      CASE WHEN s_q2 THEN e.q2_cp END,
      CASE WHEN s_q3 THEN e.q3_cp END,
      CASE WHEN s_q4 THEN e.q4_cp END,
      e.tree_path || ARRAY[e.lid::INT] AS _sort_path
    FROM enriched e

    UNION ALL

    -- ═══ Часть B: Иерархия (ROOT / GROUP / ITEM / SUB_ITEM) ═════════════════
    SELECT
      CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP'
                   WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR,
      t.depth, NULL::INT,
      CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END,
      CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END,
      t.num_group_id, t.name,
      -- Q1
      CASE WHEN s_q1 THEN a1.abi END,
      CASE WHEN s_q1 THEN a1.ai END,
      CASE WHEN s_q1 THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0) END,
      CASE WHEN s_q1 THEN a1.m1 END,
      CASE WHEN s_q1 THEN a1.m2 END,
      CASE WHEN s_q1 THEN a1.m3 END,
      CASE WHEN s_q1 THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0) END,
      CASE WHEN s_q1 THEN ROUND((COALESCE(a1.abi,0)+COALESCE(a1.ai,0)) -
                                (COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)), 1) END,
      -- Q2
      CASE WHEN s_q2 THEN NULL::NUMERIC END,
      CASE WHEN s_q2 THEN a2.abi END,
      CASE WHEN s_q2 THEN a2.ai END,
      CASE WHEN s_q2 THEN COALESCE(a2.abi,0)+COALESCE(a2.ai,0) END,
      CASE WHEN s_q2 THEN a2.m1 END,
      CASE WHEN s_q2 THEN a2.m2 END,
      CASE WHEN s_q2 THEN a2.m3 END,
      CASE WHEN s_q2 THEN COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0) END,
      CASE WHEN s_q2 THEN ROUND((COALESCE(a2.abi,0)+COALESCE(a2.ai,0)) -
                                (COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)), 1) END,
      -- Q3
      CASE WHEN s_q3 THEN NULL::NUMERIC END,
      CASE WHEN s_q3 THEN a3.abi END,
      CASE WHEN s_q3 THEN a3.ai END,
      CASE WHEN s_q3 THEN COALESCE(a3.abi,0)+COALESCE(a3.ai,0) END,
      CASE WHEN s_q3 THEN a3.m1 END,
      CASE WHEN s_q3 THEN a3.m2 END,
      CASE WHEN s_q3 THEN a3.m3 END,
      CASE WHEN s_q3 THEN COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0) END,
      CASE WHEN s_q3 THEN ROUND((COALESCE(a3.abi,0)+COALESCE(a3.ai,0)) -
                                (COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)), 1) END,
      -- Q4
      CASE WHEN s_q4 THEN NULL::NUMERIC END,
      CASE WHEN s_q4 THEN a4.abi END,
      CASE WHEN s_q4 THEN a4.ai END,
      CASE WHEN s_q4 THEN COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
      CASE WHEN s_q4 THEN a4.m1 END,
      CASE WHEN s_q4 THEN a4.m2 END,
      CASE WHEN s_q4 THEN a4.m3 END,
      CASE WHEN s_q4 THEN a4.spod END,
      CASE WHEN s_q4 THEN COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
      CASE WHEN s_q4 THEN ROUND((COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) -
                                (COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END,
      -- year
      CASE WHEN s_year THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+
                            COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
                            COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+
                            COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
      CASE WHEN s_year THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
                            COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
                            COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
                            COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
      CASE WHEN s_year THEN
        ROUND(
          (COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
           COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0))
          -
          (COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
           COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
           COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
           COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)),
          1)
      END,
      -- план-блоки (rollup)
      CASE WHEN s_q1 THEN a1.bp END,
      CASE WHEN s_q2 THEN a2.bp END,
      CASE WHEN s_q3 THEN a3.bp END,
      CASE WHEN s_q4 THEN a4.bp END,
      CASE WHEN s_year THEN COALESCE(a1.bp,0)+COALESCE(a2.bp,0)+
                           COALESCE(a3.bp,0)+COALESCE(a4.bp,0) END,
      CASE WHEN s_q2 THEN a2.cp END,
      CASE WHEN s_q3 THEN a3.cp END,
      CASE WHEN s_q4 THEN a4.cp END,
      t.path
    FROM tw t
    LEFT JOIN tw_aq1 a1 ON a1.id = t.id
    LEFT JOIN tw_aq2 a2 ON a2.id = t.id
    LEFT JOIN tw_aq3 a3 ON a3.id = t.id
    LEFT JOIN tw_aq4 a4 ON a4.id = t.id
  ) sub
  ORDER BY sub._sort_path;

END;
$function$ 
LANGUAGE plpgsql STABLE;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.upd_form3_cell / v3.upd_form3_cells — обёртки FORM_3 с проверкой роли+окна
-- ════════════════════════════════════════════════════════════════════════════
-- Параллель upd_form_cell_authz.sql, но для ветки rf_project_report. Каждая
-- ячейка прогоняется через v3.can_edit3(report_id, column, user_id). Если хоть
-- одна запрещена — RAISE EXCEPTION в формате, совместимом с конвенцией v3:
--   • role_not_allowed: <column>
--   • org_not_assigned: <column>
--   • window_closed: <column> (opens <ISO8601>)   — окно ещё не наступило
--   • window_closed: <column> (closed <ISO8601>)  — окно уже закрыто
--   • window_closed: <column> (no phase)          — фаз вообще нет
--
-- HTTP-слой парсит префикс до `:` и мапит на 403 (см. SPEC.md §5).
--
-- ⚠ Применять СТРОГО после upd_form3_cell.sql (low-level 2-арг/4-арг). Здесь
-- добавляется p_user_id → это ДРУГИЕ сигнатуры (3-арг/5-арг), которые
-- сосуществуют с low-level и делегируют в них после проверки. HTTP вызывает
-- версии с user_id. «Листа» тут нет — report_type определяется отчётом внутри
-- can_edit3.
-- ════════════════════════════════════════════════════════════════════════════

-- ─── upd_form3_cells (массовый, с авторизацией) ────────────────────────────
DROP FUNCTION IF EXISTS v3.upd_form3_cells(INT, JSONB, INT);

CREATE FUNCTION v3.upd_form3_cells(
    p_report_id INT,
    p_changes   JSONB, 
    p_user_id   INT 
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
    v_change   JSONB;
    v_column   TEXT;
    v_decision JSONB;
    v_msg      TEXT;
BEGIN
    IF p_user_id IS NULL THEN
        RAISE EXCEPTION 'role_not_allowed: <missing user_id>';
    END IF;

    IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN
        RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id;
    END IF;

    IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
        RAISE EXCEPTION 'p_changes must be a JSONB array';
    END IF;

    -- Проверяем КАЖДУЮ ячейку до начала записи. Падаем на первой запрещённой —
    -- транзакция целиком откатится, частичной записи не будет.
    FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
    LOOP
        v_column := v_change->>'column';
        IF v_column IS NULL THEN
            RAISE EXCEPTION 'change must have column: %', v_change;
        END IF;

        v_decision := v3.can_edit3(p_report_id, v_column, p_user_id);

        IF NOT (v_decision->>'ok')::boolean THEN
            IF v_decision->>'code' = 'role_not_allowed' THEN
                RAISE EXCEPTION 'role_not_allowed: %', v_column;

            ELSIF v_decision->>'code' = 'org_not_assigned' THEN
                RAISE EXCEPTION 'org_not_assigned: %', v_column;

            ELSIF v_decision->>'code' = 'window_closed' THEN
                IF v_decision ? 'opens_at' THEN
                    v_msg := format('window_closed: %s (opens %s)',
                                    v_column, v_decision->>'opens_at');
                ELSIF v_decision->>'closes_at' IS NOT NULL THEN
                    v_msg := format('window_closed: %s (closed %s)',
                                    v_column, v_decision->>'closes_at');
                ELSE
                    v_msg := format('window_closed: %s (no phase)', v_column);
                END IF;
                RAISE EXCEPTION '%', v_msg;

            ELSE
                RAISE EXCEPTION '%: %', v_decision->>'code', v_column;
            END IF;
        END IF;
    END LOOP;

    -- Все ячейки разрешены — делегируем в low-level (2-арг). ROW_UPDATE не
    -- логируем (см. logging_events.md): cell-edit'ы слишком частые.
    RETURN QUERY
    SELECT * FROM v3.upd_form3_cells(p_report_id, p_changes);
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── upd_form3_cell (одна ячейка, тонкий wrapper) ──────────────────────────
DROP FUNCTION IF EXISTS v3.upd_form3_cell(INT, INT, TEXT, JSONB, INT);

CREATE FUNCTION v3.upd_form3_cell(
    p_report_id INT,
    p_line_id   INT,
    p_column    TEXT,
    p_value     JSONB,
    p_user_id   INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
    SELECT * FROM v3.upd_form3_cells(
        p_report_id,
        jsonb_build_array(jsonb_build_object(
            'line_id', p_line_id,
            'column',  p_column,
            'value',   p_value
        )),
        p_user_id
    );
$function$ 
LANGUAGE sql VOLATILE;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.migrate_form3_v2 — DDL под переработку FORM_3 (листы ЛП/ТР, v2)
-- ════════════════════════════════════════════════════════════════════════════
-- Источник: «Пример_Форма_3_new(1).xlsm» (листы шаблон_ЛП / шаблон_ТР) + PDF-дифф
-- аналитика. Финансовая часть переразбита с 4 на 10 блоков и переведена на
-- многолетнюю раскладку (год1/год2/год3 рядом, кварталы повторяются на год).
--
-- Новые блоки внутри одного года:
--   • Базовый план по Лимиту Правления   — ручной ввод q1–q4, Год = Σ кв.
--   • Скоррект план по Лимиту Правления   — ручной ввод q2–q4 (без q1, без Года)
--   • Фактические затраты                 — уже покрыты rf_project_report_quarter
--     (adj_by_items / adj_increase / actual_m1..m3 / actual_spod), «Остаток» и
--     квартальные/годовые итоги считаются на лету в read-функции.
--
-- Многолетность уже поддержана моделью: один project → много rf_project_report
-- по (year × report_type). Здесь добавляется ТОЛЬКО хранение план-блоков и
-- «Системный код ВСП» в шапке проекта.
-- ════════════════════════════════════════════════════════════════════════════

-- ───────────── 1. rf_project_report_quarter: план-блоки ─────────────────────
-- base_plan      — «Базовый план по Лимиту Правления», поквартально (q1–q4).
-- corrected_plan — «Скоррект план по Лимиту Правления», поквартально (q2–q4).
--                  Для q1 значения не существует (шаблон рисует скорр.план с
--                  II квартала) — гарантируется CHECK ниже + write-слой/RBAC.
ALTER TABLE v3.rf_project_report_quarter
  ADD COLUMN IF NOT EXISTS base_plan      NUMERIC,
  ADD COLUMN IF NOT EXISTS corrected_plan NUMERIC;

-- Скоррект план не заводится в I квартале (нет колонки в шаблоне).
ALTER TABLE v3.rf_project_report_quarter
  DROP CONSTRAINT IF EXISTS chk_v3_rf_quarter_corr_plan_q1;
ALTER TABLE v3.rf_project_report_quarter
  ADD CONSTRAINT chk_v3_rf_quarter_corr_plan_q1
    CHECK (quarter <> 1 OR corrected_plan IS NULL);

-- ───────────── 2. v3.project: «Системный код ВСП» ───────────────────────────
-- Новое поле шапки «Информация о проекте ВСП» (в примере — 1808). Выбор из
-- справочника ИНФО / выпадающего списка; хранится как атрибут проекта (вводится
-- один раз, не дублируется между треками/годами — как и остальные атрибуты ВСП).
ALTER TABLE v3.project
  ADD COLUMN IF NOT EXISTS system_code VARCHAR;

-- ───────────── 3. Обновление энум-списков шапки ────────────────────────────
-- В новом шаблоне списки изменились относительно старого VBA-набора.
-- project_type: новый перечень из 8 значений (шаблон_ЛП F2:F9).
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_project_type_check;
ALTER TABLE v3.project
  ADD CONSTRAINT project_project_type_check
    CHECK (project_type IS NULL OR project_type IN (
      'Открытие ВСП','Реновация ВСП','Закрытие ВСП',
      'Реновация РФ','Закрытие РФ',
      'Открытие УРМ','Реновация УРМ','Закрытие УРМ'
    ));

-- vsp_format («Новый формат ВСП») — поле автозаполняется из справочника INFO
-- («Утвержденный формат»), не вводится вручную и его словарь ведётся как
-- reference-data. Жёсткий CHECK снимаем, чтобы новые форматы не ломали вставку.
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_vsp_format_check;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.upd_form3_cell / v3.upd_form3_cells — точечная правка ячейки FORM_3.
-- Хранение FORM_3 идёт через rf_project_report → rf_project_report_line →
-- rf_project_report_quarter (PK = (rf_project_report_line_id, quarter)).
-- API симметричен upd_form_cell, но идентификатор «строки» — это
-- rf_project_report_line.id (то же `line_id`, что в JSONB read-функции).
--
-- Поддерживаемые ключи p_column (см. v_form3_report_jsonb.sql):
--   Факт:        'qN.adj_by_items', 'qN.adj_increase', 'qN.m1', 'qN.m2', 'qN.m3'
--                (+ 'q4.spod' — только IV квартал)
--   Базовый план:   'q1.base_plan' … 'q4.base_plan'      (все 4 квартала)
--   Скоррект план:  'q2.corrected_plan' … 'q4.corrected_plan'
--                   (без q1 — шаблон рисует скорр.план с II квартала)
--
-- Computed (RO): qN.total_corr, qN.quarter_actual, qN.economy (=«Остаток»),
-- qN.carryover, base_plan.year / totals.* (годовые итоги считаются на лету).
--
-- Атрибуты rf_project_report (dev_type, vsp_format, object_address, ...) —
-- НЕ через эту функцию (это header отчёта, не ячейка строки).
-- ════════════════════════════════════════════════════════════════════════════

DROP FUNCTION IF EXISTS v3._apply_form3_cell(INT, TEXT, JSONB);

CREATE FUNCTION v3._apply_form3_cell(
    p_line_id INT,                  -- rf_project_report_line.id
    p_column  TEXT,
    p_value   JSONB
)
RETURNS VOID
AS $function$
DECLARE
  v_parts       TEXT[];
  v_scope       TEXT;
  v_field       TEXT;
  v_q           SMALLINT;
  v_target_col  TEXT;
  v_target_type TEXT;
  v_sql         TEXT;
BEGIN
  v_parts := string_to_array(p_column, '.');
  IF array_length(v_parts, 1) < 2 THEN
    RAISE EXCEPTION 'bad_column_format: %, expected scope.field', p_column;
  END IF;
  v_scope := v_parts[1];
  v_field := v_parts[2];

  IF v_scope = 'totals' OR v_field IN ('total_corr','quarter_actual','economy','carryover') THEN
    RAISE EXCEPTION 'computed_field: %', p_column;
  END IF;

  IF v_scope NOT IN ('q1','q2','q3','q4') THEN
    RAISE EXCEPTION 'unknown_scope: % (FORM_3 supports only q1..q4)', v_scope;
  END IF;

  v_q := substring(v_scope FROM 2)::SMALLINT;

  -- Маппинг JSONB-ключ → колонка rf_project_report_quarter
  CASE v_field
    WHEN 'adj_by_items' THEN v_target_col := 'adj_by_items'; v_target_type := 'NUMERIC';
    WHEN 'adj_increase' THEN v_target_col := 'adj_increase'; v_target_type := 'NUMERIC';
    WHEN 'm1'           THEN v_target_col := 'actual_m1';    v_target_type := 'NUMERIC';
    WHEN 'm2'           THEN v_target_col := 'actual_m2';    v_target_type := 'NUMERIC';
    WHEN 'm3'           THEN v_target_col := 'actual_m3';    v_target_type := 'NUMERIC';
    WHEN 'spod' THEN
      IF v_q <> 4 THEN RAISE EXCEPTION 'spod is q4-only: %', p_column; END IF;
      v_target_col := 'actual_spod'; v_target_type := 'NUMERIC';
    WHEN 'base_plan'      THEN v_target_col := 'base_plan';      v_target_type := 'NUMERIC';
    WHEN 'corrected_plan' THEN
      IF v_q = 1 THEN RAISE EXCEPTION 'corrected_plan is q2-q4 only: %', p_column; END IF;
      v_target_col := 'corrected_plan'; v_target_type := 'NUMERIC';
    ELSE
      RAISE EXCEPTION 'unknown_column: %', p_column;
  END CASE;

  v_sql := format(
    'INSERT INTO v3.rf_project_report_quarter (rf_project_report_line_id, quarter, %1$I) '
    || 'VALUES ($1, $2, ($3 #>> ''{}'')::%2$s) '
    || 'ON CONFLICT (rf_project_report_line_id, quarter) DO UPDATE SET %1$I = EXCLUDED.%1$I',
    v_target_col, v_target_type
  );
  EXECUTE v_sql USING p_line_id, v_q, p_value;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── Public batch ──────────────────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.upd_form3_cells(INT, JSONB);

CREATE FUNCTION v3.upd_form3_cells(
    p_report_id INT,
    p_changes   JSONB
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
  v_change         JSONB;
  v_line_id        INT;
  v_column         TEXT;
  v_value          JSONB;
  v_changed_lines  INT[] := ARRAY[]::INT[];
BEGIN
  IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
    RAISE EXCEPTION 'p_changes must be a JSONB array';
  END IF;

  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;

  FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
  LOOP
    v_line_id := (v_change->>'line_id')::INT;
    v_column  := v_change->>'column';
    v_value   := v_change->'value';

    IF v_line_id IS NULL OR v_column IS NULL THEN
      RAISE EXCEPTION 'change must have line_id and column: %', v_change;
    END IF;

    IF NOT EXISTS (
      SELECT 1 FROM v3.rf_project_report_line
       WHERE id = v_line_id AND rf_project_report_id = p_report_id
    ) THEN
      RAISE EXCEPTION 'rf_project_report_line id=% не принадлежит report id=%',
        v_line_id, p_report_id;
    END IF;

    PERFORM v3._apply_form3_cell(v_line_id, v_column, v_value);

    IF NOT (v_line_id = ANY(v_changed_lines)) THEN
      v_changed_lines := v_changed_lines || v_line_id;
    END IF;
  END LOOP;

  -- Возврат: иерархия по пути изменённых rfprl + изменённые INPUT по line_id
  DECLARE
    v_eid_set INT[];
    v_ei_path INT[];
  BEGIN
    SELECT array_agg(DISTINCT rfprl.expense_item_id)
      INTO v_eid_set
      FROM v3.rf_project_report_line rfprl
     WHERE rfprl.id = ANY(v_changed_lines);
    v_ei_path := v3._expense_item_path_set(v_eid_set);

    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form3_report_jsonb(p_report_id, NULL) v
     WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
            AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
        OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(v_changed_lines))
     ORDER BY v.sort_order;
  END;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── Public single-cell wrapper ────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.upd_form3_cell(INT, INT, TEXT, JSONB);

CREATE FUNCTION v3.upd_form3_cell(
    p_report_id INT,
    p_line_id   INT,
    p_column    TEXT,
    p_value     JSONB
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
  SELECT * FROM v3.upd_form3_cells(
    p_report_id,
    jsonb_build_array(jsonb_build_object(
      'line_id', p_line_id,
      'column',  p_column,
      'value',   p_value
    ))
  );
$function$ 
LANGUAGE sql VOLATILE;
-- ════════════════════════════════════════════════════════════════════════════
-- FORM_3 write API: project header + line ADD/DEL + project ADD-master
-- ════════════════════════════════════════════════════════════════════════════
-- Дополняет существующий v3.upd_form3_cell (правка ячеек квартальных
-- значений в rf_project_report_quarter) — здесь функции для:
--   1) v3.upd_project              — правка атрибутов карточки проекта
--   2) v3.add_form3_line           — [DEPRECATED в UI] добавить позицию вручную.
--   3) v3.del_form3_line           — [DEPRECATED в UI] удалить позицию.
--      В FORM_3 сетка статей фиксирована (нет макроса добавления строк) —
--      строки засеваются автоматически, юзер пишет прямо в строку статьи.
--      add/del_form3_line оставлены для админ-правок/legacy.
--   3b) v3.seed_form3_report_lines — засев полного набора листовых статей FORM_3.
--   3c) v3.add_project_year        — динамическое дорастание проекта годом
--                                    (2 отчёта + snapshot фаз + сетка статей).
--   4) v3.add_project              — мастер: project + первый год (add_project_year).
-- ════════════════════════════════════════════════════════════════════════════

-- ─── 1. upd_project ────────────────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.upd_project(INT, TEXT, JSONB);

CREATE FUNCTION v3.upd_project(
    p_project_id INT,
    p_column     TEXT,
    p_value      JSONB
)
RETURNS JSONB    -- весь header проекта после правки
AS $function$
DECLARE
  v_target_col  TEXT;
  v_target_type TEXT;
  v_sql         TEXT;
  v_out         JSONB;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
    RAISE EXCEPTION 'project id=% не существует', p_project_id;
  END IF;

  -- Editable атрибуты (CHECK-ы из migrate_form3_v1.sql применятся при UPDATE).
  -- Не editable: level (структура), parent_id (иерархия).
  CASE p_column
    WHEN 'name'           THEN v_target_col := 'name';           v_target_type := 'TEXT';
    WHEN 'project_type'   THEN v_target_col := 'project_type';   v_target_type := 'TEXT';
    WHEN 'vsp_format'     THEN v_target_col := 'vsp_format';     v_target_type := 'TEXT';
    WHEN 'placement_type' THEN v_target_col := 'placement_type'; v_target_type := 'TEXT';
    WHEN 'object_address' THEN v_target_col := 'object_address'; v_target_type := 'TEXT';
    WHEN 'system_code'    THEN v_target_col := 'system_code';    v_target_type := 'TEXT';
    WHEN 'staff_count'    THEN v_target_col := 'staff_count';    v_target_type := 'INTEGER';
    WHEN 'total_area'     THEN v_target_col := 'total_area';     v_target_type := 'NUMERIC';
    WHEN 'org_unit_id'    THEN v_target_col := 'org_unit_id';    v_target_type := 'INTEGER';
    WHEN 'level','parent_id' THEN
      RAISE EXCEPTION 'structural_field: % (правится отдельно через ADD project / структурный API)', p_column;
    WHEN 'id' THEN
      RAISE EXCEPTION 'key_field: id (RO)';
    ELSE
      RAISE EXCEPTION 'unknown_column: %', p_column;
  END CASE;

  -- FK санити (если применимо)
  IF v_target_col = 'org_unit_id' AND p_value <> 'null'::jsonb THEN
    IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = (p_value #>> '{}')::INT) THEN
      RAISE EXCEPTION 'org_unit id=% не существует', (p_value #>> '{}');
    END IF;
  END IF;

  v_sql := format(
    'UPDATE v3.project SET %1$I = ($2 #>> ''{}'')::%2$s WHERE id = $1',
    v_target_col, v_target_type
  );
  EXECUTE v_sql USING p_project_id, p_value;

  -- Возврат — весь header проекта
  SELECT jsonb_build_object(
           'id',             p.id,
           'name',           p.name,
           'level',          p.level,
           'parent_id',      p.parent_id,
           'org_unit_id',    p.org_unit_id,
           'project_type',   p.project_type,
           'vsp_format',     p.vsp_format,
           'placement_type', p.placement_type,
           'object_address', p.object_address,
           'system_code',    p.system_code,
           'staff_count',    p.staff_count,
           'total_area',     p.total_area
         )
    INTO v_out
    FROM v3.project p WHERE p.id = p_project_id;
  RETURN v_out;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── 2. add_form3_line ─────────────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.add_form3_line(INT, INT);

CREATE FUNCTION v3.add_form3_line(
    p_report_id        INT,
    p_expense_item_id  INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
  v_new_id INT;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.expense_item WHERE id = p_expense_item_id) THEN
    RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.expense_item_form_type
                  WHERE expense_item_id = p_expense_item_id AND form_type_code = 'FORM_3') THEN
    RAISE EXCEPTION 'expense_item id=% не привязан к form_type FORM_3', p_expense_item_id;
  END IF;

  INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id)
  VALUES (p_report_id, p_expense_item_id)
  RETURNING id INTO v_new_id;

  -- Аудит: ROW_CREATE
  PERFORM v3.log_event(
    'ROW_CREATE', 'ROW_CREATE',
    jsonb_build_object(
      'sheet',           'FORM_3',
      'line_id',         v_new_id,
      'report_id',       p_report_id,
      'expense_item_id', p_expense_item_id
    )
  );

  -- Возврат: только иерархия по пути нового ei + новая INPUT
  DECLARE v_ei_path INT[];
  BEGIN
    v_ei_path := v3._expense_item_path_set(ARRAY[p_expense_item_id]);
    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form3_report_jsonb(p_report_id, NULL) v
     WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
            AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
        OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
     ORDER BY v.sort_order;
  END;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── 3. del_form3_line ─────────────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.del_form3_line(INT);

CREATE FUNCTION v3.del_form3_line(
    p_line_id INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
  v_report_id INT;
  v_eid       INT;
  v_ei_path   INT[];
BEGIN
  -- Извлекаем report_id + ei ДО DELETE (нужен для path)
  SELECT rfprl.rf_project_report_id, rfprl.expense_item_id
    INTO v_report_id, v_eid
    FROM v3.rf_project_report_line rfprl
   WHERE rfprl.id = p_line_id;
  IF v_report_id IS NULL THEN
    RAISE EXCEPTION 'rf_project_report_line id=% не существует', p_line_id;
  END IF;

  v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);

  -- Каскад (FK без ON DELETE CASCADE)
  DELETE FROM v3.rf_project_report_quarter WHERE rf_project_report_line_id = p_line_id;
  DELETE FROM v3.rf_project_report_line    WHERE id = p_line_id;

  -- Аудит: ROW_DELETE
  PERFORM v3.log_event(
    'ROW_DELETE', 'ROW_DELETE',
    jsonb_build_object(
      'sheet',           'FORM_3',
      'line_id',         p_line_id,
      'report_id',       v_report_id,
      'expense_item_id', v_eid
    )
  );

  -- Возврат: только иерархия по пути удалённой (INPUT уже нет)
  RETURN QUERY
  SELECT v.row_type, v.depth, v.sort_order, v.data
    FROM v3.v_form3_report_jsonb(v_report_id, NULL) v
   WHERE v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
     AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
   ORDER BY v.sort_order;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── 3b. seed_form3_report_lines (фиксированная сетка статей) ──────────────
-- В FORM_3 строки НЕ добавляются пользователем (в шаблоне нет макроса добавления
-- строк) — сетка статей фиксирована, юзер пишет прямо в строку статьи. Поэтому
-- при создании отчёта засеваем ПОЛНЫЙ набор листовых статей FORM_3 (листовая =
-- FORM_3-статья без FORM_3-детей). Идемпотентно (uq_v3_rf_project_report_line).
-- Возвращает число вставленных строк.
DROP FUNCTION IF EXISTS v3.seed_form3_report_lines(INT);

CREATE FUNCTION v3.seed_form3_report_lines(p_report_id INT)
RETURNS INT
AS $function$
DECLARE
  v_cnt INT;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;

  INSERT INTO v3.rf_project_report_line (rf_project_report_id, expense_item_id)
  SELECT p_report_id, e.id
    FROM v3.expense_item e
    JOIN v3.expense_item_form_type eft
      ON eft.expense_item_id = e.id AND eft.form_type_code = 'FORM_3'
   WHERE NOT EXISTS (
           SELECT 1 FROM v3.expense_item c
            JOIN v3.expense_item_form_type eftc
              ON eftc.expense_item_id = c.id AND eftc.form_type_code = 'FORM_3'
           WHERE c.parent_id = e.id
         )
  ON CONFLICT (rf_project_report_id, expense_item_id) DO NOTHING;

  GET DIAGNOSTICS v_cnt = ROW_COUNT;
  RETURN v_cnt;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── 3c. add_project_year (динамическое дорастание проекта по годам) ───────
-- Проект может дополняться годами в процессе работы. Заводит пару отчётов
-- (LIMIT + CURRENT_EXPENSES) на (project, year), снапшотит RBAC-фазы и засевает
-- фиксированную сетку статей. Год несёт report_id — column_keys без префикса
-- года, RBAC работает пофазно на каждый год-отчёт (см. seed_phase_template).
DROP FUNCTION IF EXISTS v3.add_project_year(INT, INT, BOOLEAN);

CREATE FUNCTION v3.add_project_year(
    p_project_id INT,
    p_year       INT,
    p_emit_audit BOOLEAN DEFAULT true  -- false когда зовётся из add_project (там PROJECT_CREATE)
)
RETURNS TABLE (
    limit_report_id             INT,
    current_expenses_report_id  INT
)
AS $function$
DECLARE
  v_lim INT;
  v_cur INT;
BEGIN
  IF p_year IS NULL THEN
    RAISE EXCEPTION 'p_year обязателен';
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
    RAISE EXCEPTION 'project id=% не существует', p_project_id;
  END IF;
  IF EXISTS (SELECT 1 FROM v3.rf_project_report
              WHERE project_id = p_project_id AND year = p_year) THEN
    RAISE EXCEPTION 'год % у проекта id=% уже заведён', p_year, p_project_id;
  END IF;

  INSERT INTO v3.rf_project_report (project_id, year, report_type)
  VALUES (p_project_id, p_year, 'LIMIT')            RETURNING id INTO v_lim;
  INSERT INTO v3.rf_project_report (project_id, year, report_type)
  VALUES (p_project_id, p_year, 'CURRENT_EXPENSES') RETURNING id INTO v_cur;

  -- Snapshot RBAC-фаз (иначе default deny → отчёт read-only)
  PERFORM v3.copy_template_to_report(v_lim);
  PERFORM v3.copy_template_to_report(v_cur);

  -- Фиксированная сетка статей на оба трека
  PERFORM v3.seed_form3_report_lines(v_lim);
  PERFORM v3.seed_form3_report_lines(v_cur);

  IF p_emit_audit THEN
    PERFORM v3.log_event(
      'PROJECT_YEAR_ADD', 'PROJECT_YEAR_ADD',
      jsonb_build_object(
        'project_id',                 p_project_id,
        'year',                       p_year,
        'limit_report_id',            v_lim,
        'current_expenses_report_id', v_cur
      )
    );
  END IF;

  RETURN QUERY SELECT v_lim, v_cur;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── 4. add_project (мастер: project + первый год через add_project_year) ──
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT, INT,     VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT,          VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC);
DROP FUNCTION IF EXISTS v3.add_project(VARCHAR, INT, INT, VARCHAR, INT,          VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, NUMERIC, VARCHAR);

CREATE FUNCTION v3.add_project(
    p_name           VARCHAR,
    p_year           INT,
    p_org_unit_id    INT,
    p_level          VARCHAR DEFAULT 'project',  -- 'program' | 'project'
    p_parent_id      INT     DEFAULT NULL,       -- обязателен если level='project' и есть программа
    p_project_type   VARCHAR DEFAULT NULL,
    p_vsp_format     VARCHAR DEFAULT NULL,
    p_placement_type VARCHAR DEFAULT NULL,
    p_object_address VARCHAR DEFAULT NULL,
    p_staff_count    INT     DEFAULT NULL,
    p_total_area     NUMERIC DEFAULT NULL,
    p_system_code    VARCHAR DEFAULT NULL        -- Системный код ВСП
)
RETURNS TABLE (
    project_id                  INT,
    limit_report_id             INT,
    current_expenses_report_id  INT
)
AS $function$
DECLARE
  v_pid INT;
  v_lim INT;
  v_cur INT;
BEGIN
  IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
    RAISE EXCEPTION 'p_name обязателен';
  END IF;
  IF p_year IS NULL THEN
    RAISE EXCEPTION 'p_year обязателен';
  END IF;
  IF p_org_unit_id IS NULL THEN
    RAISE EXCEPTION 'p_org_unit_id обязателен';
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
    RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
  END IF;
  IF p_parent_id IS NOT NULL AND
     NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
    RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
  END IF;

  -- INSERT project (CHECK-ы name/project_type/vsp_format/placement_type
  -- из migrate_form3_v1.sql применятся автоматически).
  INSERT INTO v3.project (
    name, level, parent_id, org_unit_id,
    project_type, vsp_format, placement_type, object_address, staff_count, total_area,
    system_code
  ) VALUES (
    p_name, p_level, p_parent_id, p_org_unit_id,
    p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area,
    p_system_code
  ) RETURNING id INTO v_pid;

  -- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
  -- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие
  -- годы добавляются отдельно через v3.add_project_year.
  SELECT y.limit_report_id, y.current_expenses_report_id
    INTO v_lim, v_cur
    FROM v3.add_project_year(v_pid, p_year, false) y;

  -- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
  PERFORM v3.log_event(
    'PROJECT_CREATE', 'PROJECT_CREATE',
    jsonb_strip_nulls(jsonb_build_object(
      'project_id',     v_pid,
      'name',           p_name,
      'year',           p_year,
      'level',          p_level,
      'parent_id',      p_parent_id,
      'org_unit_id',    p_org_unit_id,
      'project_type',   p_project_type,
      'vsp_format',     p_vsp_format,
      'placement_type', p_placement_type,
      'object_address', p_object_address,
      'system_code',    p_system_code,
      'staff_count',    p_staff_count,
      'total_area',     p_total_area,
      'limit_report_id',            v_lim,
      'current_expenses_report_id', v_cur
    ))
  );

  RETURN QUERY SELECT v_pid, v_lim, v_cur;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.v_form3_report_sections — отображение листа ЛП (LIMIT) или ТР (CURRENT)
-- ════════════════════════════════════════════════════════════════════════════
-- Возвращает иерархию (ROOT/GROUP/ITEM/SUB_ITEM) + INPUT строки в tree-order
-- для одного rf_project_report (один трек: report_type = LIMIT либо
-- CURRENT_EXPENSES). UI выбирает трек, передавая report_id соответствующей
-- записи rf_project_report.
--
-- Колоночная раскладка повторяет шаблон Формы 3:
--   header, q1, q2, q3, q4, year_totals.
--
-- Q1: adj_by_items, adj_increase, total_corr, m1, m2, m3, quarter_actual, economy
-- Q2/Q3: carryover, adj_by_items, adj_increase, total_corr, m1..m3, quarter_actual, economy
-- Q4: carryover, adj_by_items, adj_increase, total_corr, m1..m3, spod, quarter_actual, economy
-- year_totals: total_year_corr, total_year_actual, economy_year
--
-- p_sections — массив включаемых блоков (NULL = все). Допустимые имена:
--   'q1', 'q2', 'q3', 'q4', 'year'
-- ════════════════════════════════════════════════════════════════════════════

DROP FUNCTION IF EXISTS v3.v_form3_report_sections(INT, TEXT[]);

CREATE FUNCTION v3.v_form3_report_sections(
    p_report_id INT,
    p_sections  TEXT[] DEFAULT NULL
)
RETURNS TABLE (
    row_type             VARCHAR,
    depth                INT,
    line_id              INT,
    col_section_code     VARCHAR,
    col_item_id          VARCHAR,
    col_num_group_id     VARCHAR,
    col_name             VARCHAR,
    -- Q1
    col_q1_adj_by_items  NUMERIC,
    col_q1_adj_increase  NUMERIC,
    col_q1_total_corr    NUMERIC,
    col_q1_m1            NUMERIC,
    col_q1_m2            NUMERIC,
    col_q1_m3            NUMERIC,
    col_q1_quarter_actual NUMERIC,
    col_q1_economy       NUMERIC,
    -- Q2
    col_q2_carryover     NUMERIC,
    col_q2_adj_by_items  NUMERIC,
    col_q2_adj_increase  NUMERIC,
    col_q2_total_corr    NUMERIC,
    col_q2_m1            NUMERIC,
    col_q2_m2            NUMERIC,
    col_q2_m3            NUMERIC,
    col_q2_quarter_actual NUMERIC,
    col_q2_economy       NUMERIC,
    -- Q3
    col_q3_carryover     NUMERIC,
    col_q3_adj_by_items  NUMERIC,
    col_q3_adj_increase  NUMERIC,
    col_q3_total_corr    NUMERIC,
    col_q3_m1            NUMERIC,
    col_q3_m2            NUMERIC,
    col_q3_m3            NUMERIC,
    col_q3_quarter_actual NUMERIC,
    col_q3_economy       NUMERIC,
    -- Q4
    col_q4_carryover     NUMERIC,
    col_q4_adj_by_items  NUMERIC,
    col_q4_adj_increase  NUMERIC,
    col_q4_total_corr    NUMERIC,
    col_q4_m1            NUMERIC,
    col_q4_m2            NUMERIC,
    col_q4_m3            NUMERIC,
    col_q4_spod          NUMERIC,
    col_q4_quarter_actual NUMERIC,
    col_q4_economy       NUMERIC,
    -- year totals
    col_year_total_corr   NUMERIC,
    col_year_total_actual NUMERIC,
    col_year_economy      NUMERIC,
    -- план-блоки (v2): Базовый план (q1–q4 + Год) и Скоррект план (q2–q4)
    col_q1_base_plan      NUMERIC,
    col_q2_base_plan      NUMERIC,
    col_q3_base_plan      NUMERIC,
    col_q4_base_plan      NUMERIC,
    col_year_base_plan    NUMERIC,
    col_q2_corrected_plan NUMERIC,
    col_q3_corrected_plan NUMERIC,
    col_q4_corrected_plan NUMERIC,
    _sort_path            INT[]
)
AS $function$
DECLARE
    s_q1   BOOLEAN := p_sections IS NULL OR 'q1'   = ANY(p_sections);
    s_q2   BOOLEAN := p_sections IS NULL OR 'q2'   = ANY(p_sections);
    s_q3   BOOLEAN := p_sections IS NULL OR 'q3'   = ANY(p_sections);
    s_q4   BOOLEAN := p_sections IS NULL OR 'q4'   = ANY(p_sections);
    s_year BOOLEAN := p_sections IS NULL OR 'year' = ANY(p_sections);
BEGIN
  IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN
    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
  END IF;

  RETURN QUERY
  WITH
    -- Дерево expense_item, ограниченное FORM_3 (через junction)
    tw AS (
      SELECT t.* FROM v3.mv_expense_item_tree t
      JOIN v3.expense_item_form_type eft
        ON eft.expense_item_id = t.id AND eft.form_type_code = 'FORM_3'
    ),
    -- INPUT строки: одна на v3.rf_project_report_line
    page AS (
      SELECT l.id AS lid, l.expense_item_id AS eid,
             t.parent_item_id AS sc,        -- section-код родителя в дереве (1.01.1.)
             ei.item_id AS ic,
             ei.num_group_id AS ng, ei.name AS ename,
             t.path AS tree_path
      FROM v3.rf_project_report_line l
      JOIN v3.expense_item ei ON ei.id = l.expense_item_id
      JOIN tw t ON t.id = l.expense_item_id
      WHERE l.rf_project_report_id = p_report_id
    ),
    -- Поквартальные данные
    jq AS (
      SELECT q.rf_project_report_line_id AS lid, q.quarter,
             q.adj_by_items, q.adj_increase,
             q.actual_m1, q.actual_m2, q.actual_m3, q.actual_spod,
             q.base_plan, q.corrected_plan
      FROM v3.rf_project_report_quarter q
      JOIN v3.rf_project_report_line l ON l.id = q.rf_project_report_line_id
      WHERE l.rf_project_report_id = p_report_id
    ),
    -- INPUT base: соединяем page + 4 квартала
    input_base AS (
      SELECT
        pg.lid, pg.eid, pg.sc, pg.ic, pg.ng, pg.ename, pg.tree_path,
        q1.adj_by_items AS q1_abi, q1.adj_increase AS q1_ai,
        q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3,
        q2.adj_by_items AS q2_abi, q2.adj_increase AS q2_ai,
        q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3,
        q3.adj_by_items AS q3_abi, q3.adj_increase AS q3_ai,
        q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3,
        q4.adj_by_items AS q4_abi, q4.adj_increase AS q4_ai,
        q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3,
        q4.actual_spod AS q4_spod,
        q1.base_plan AS q1_bp, q2.base_plan AS q2_bp,
        q3.base_plan AS q3_bp, q4.base_plan AS q4_bp,
        q2.corrected_plan AS q2_cp, q3.corrected_plan AS q3_cp, q4.corrected_plan AS q4_cp
      FROM page pg
      LEFT JOIN jq q1 ON q1.lid = pg.lid AND q1.quarter = 1
      LEFT JOIN jq q2 ON q2.lid = pg.lid AND q2.quarter = 2
      LEFT JOIN jq q3 ON q3.lid = pg.lid AND q3.quarter = 3
      LEFT JOIN jq q4 ON q4.lid = pg.lid AND q4.quarter = 4
    ),
    -- Computed totals per row
    enriched AS (
      SELECT b.*,
        COALESCE(b.q1_abi,0) + COALESCE(b.q1_ai,0) AS q1_tc,
        COALESCE(b.q1_m1,0) + COALESCE(b.q1_m2,0) + COALESCE(b.q1_m3,0) AS q1_qa,
        COALESCE(b.q2_abi,0) + COALESCE(b.q2_ai,0) AS q2_tc,
        COALESCE(b.q2_m1,0) + COALESCE(b.q2_m2,0) + COALESCE(b.q2_m3,0) AS q2_qa,
        COALESCE(b.q3_abi,0) + COALESCE(b.q3_ai,0) AS q3_tc,
        COALESCE(b.q3_m1,0) + COALESCE(b.q3_m2,0) + COALESCE(b.q3_m3,0) AS q3_qa,
        COALESCE(b.q4_abi,0) + COALESCE(b.q4_ai,0) AS q4_tc,
        COALESCE(b.q4_m1,0) + COALESCE(b.q4_m2,0) + COALESCE(b.q4_m3,0) + COALESCE(b.q4_spod,0) AS q4_qa
      FROM input_base b
    ),
    -- Per-line aggregates for tree rollup
    agg_q AS (
      SELECT l.expense_item_id AS eid, q.quarter,
        SUM(q.adj_by_items) AS abi, SUM(q.adj_increase) AS ai,
        SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3,
        SUM(q.actual_spod) AS spod,
        SUM(q.base_plan) AS bp, SUM(q.corrected_plan) AS cp
      FROM v3.rf_project_report_line l
      JOIN v3.rf_project_report_quarter q ON q.rf_project_report_line_id = l.id
      WHERE l.rf_project_report_id = p_report_id
      GROUP BY l.expense_item_id, q.quarter
    ),
    tw_aq1 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.bp) AS bp
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 1
      GROUP BY tw.id
    ),
    tw_aq2 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.bp) AS bp, SUM(a.cp) AS cp
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 2
      GROUP BY tw.id
    ),
    tw_aq3 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.bp) AS bp, SUM(a.cp) AS cp
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 3
      GROUP BY tw.id
    ),
    tw_aq4 AS (
      SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
             SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
             SUM(a.spod) AS spod,
             SUM(a.bp) AS bp, SUM(a.cp) AS cp
      FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 4
      GROUP BY tw.id
    )

  -- ═══ Часть A: INPUT строки ════════════════════════════════════════════════
  SELECT * FROM (
    SELECT
      'INPUT'::VARCHAR AS row_type, 3 AS depth, e.lid::INT AS line_id,
      e.sc, e.ic, e.ng, e.ename,
      -- Q1
      CASE WHEN s_q1 THEN e.q1_abi END,
      CASE WHEN s_q1 THEN e.q1_ai END,
      CASE WHEN s_q1 THEN e.q1_tc END,
      CASE WHEN s_q1 THEN e.q1_m1 END,
      CASE WHEN s_q1 THEN e.q1_m2 END,
      CASE WHEN s_q1 THEN e.q1_m3 END,
      CASE WHEN s_q1 THEN e.q1_qa END,
      CASE WHEN s_q1 THEN ROUND(e.q1_tc - e.q1_qa, 1) END,
      -- Q2 (carryover пока NULL — нет столбца в _quarter; задел на будущее)
      CASE WHEN s_q2 THEN NULL::NUMERIC END,
      CASE WHEN s_q2 THEN e.q2_abi END,
      CASE WHEN s_q2 THEN e.q2_ai END,
      CASE WHEN s_q2 THEN e.q2_tc END,
      CASE WHEN s_q2 THEN e.q2_m1 END,
      CASE WHEN s_q2 THEN e.q2_m2 END,
      CASE WHEN s_q2 THEN e.q2_m3 END,
      CASE WHEN s_q2 THEN e.q2_qa END,
      CASE WHEN s_q2 THEN ROUND(e.q2_tc - e.q2_qa, 1) END,
      -- Q3
      CASE WHEN s_q3 THEN NULL::NUMERIC END,
      CASE WHEN s_q3 THEN e.q3_abi END,
      CASE WHEN s_q3 THEN e.q3_ai END,
      CASE WHEN s_q3 THEN e.q3_tc END,
      CASE WHEN s_q3 THEN e.q3_m1 END,
      CASE WHEN s_q3 THEN e.q3_m2 END,
      CASE WHEN s_q3 THEN e.q3_m3 END,
      CASE WHEN s_q3 THEN e.q3_qa END,
      CASE WHEN s_q3 THEN ROUND(e.q3_tc - e.q3_qa, 1) END,
      -- Q4
      CASE WHEN s_q4 THEN NULL::NUMERIC END,
      CASE WHEN s_q4 THEN e.q4_abi END,
      CASE WHEN s_q4 THEN e.q4_ai END,
      CASE WHEN s_q4 THEN e.q4_tc END,
      CASE WHEN s_q4 THEN e.q4_m1 END,
      CASE WHEN s_q4 THEN e.q4_m2 END,
      CASE WHEN s_q4 THEN e.q4_m3 END,
      CASE WHEN s_q4 THEN e.q4_spod END,
      CASE WHEN s_q4 THEN e.q4_qa END,
      CASE WHEN s_q4 THEN ROUND(e.q4_tc - e.q4_qa, 1) END,
      -- year
      CASE WHEN s_year THEN e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc END,
      CASE WHEN s_year THEN e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa END,
      CASE WHEN s_year THEN ROUND((e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc) -
                                  (e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa), 1) END,
      -- план-блоки
      CASE WHEN s_q1 THEN e.q1_bp END,
      CASE WHEN s_q2 THEN e.q2_bp END,
      CASE WHEN s_q3 THEN e.q3_bp END,
      CASE WHEN s_q4 THEN e.q4_bp END,
      CASE WHEN s_year THEN COALESCE(e.q1_bp,0)+COALESCE(e.q2_bp,0)+
                           COALESCE(e.q3_bp,0)+COALESCE(e.q4_bp,0) END,
      CASE WHEN s_q2 THEN e.q2_cp END,
      CASE WHEN s_q3 THEN e.q3_cp END,
      CASE WHEN s_q4 THEN e.q4_cp END,
      e.tree_path || ARRAY[e.lid::INT] AS _sort_path
    FROM enriched e

    UNION ALL

    -- ═══ Часть B: Иерархия (ROOT / GROUP / ITEM / SUB_ITEM) ═════════════════
    SELECT
      CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP'
                   WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR,
      t.depth, NULL::INT,
      CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END,
      CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END,
      t.num_group_id, t.name,
      -- Q1
      CASE WHEN s_q1 THEN a1.abi END,
      CASE WHEN s_q1 THEN a1.ai END,
      CASE WHEN s_q1 THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0) END,
      CASE WHEN s_q1 THEN a1.m1 END,
      CASE WHEN s_q1 THEN a1.m2 END,
      CASE WHEN s_q1 THEN a1.m3 END,
      CASE WHEN s_q1 THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0) END,
      CASE WHEN s_q1 THEN ROUND((COALESCE(a1.abi,0)+COALESCE(a1.ai,0)) -
                                (COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)), 1) END,
      -- Q2
      CASE WHEN s_q2 THEN NULL::NUMERIC END,
      CASE WHEN s_q2 THEN a2.abi END,
      CASE WHEN s_q2 THEN a2.ai END,
      CASE WHEN s_q2 THEN COALESCE(a2.abi,0)+COALESCE(a2.ai,0) END,
      CASE WHEN s_q2 THEN a2.m1 END,
      CASE WHEN s_q2 THEN a2.m2 END,
      CASE WHEN s_q2 THEN a2.m3 END,
      CASE WHEN s_q2 THEN COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0) END,
      CASE WHEN s_q2 THEN ROUND((COALESCE(a2.abi,0)+COALESCE(a2.ai,0)) -
                                (COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)), 1) END,
      -- Q3
      CASE WHEN s_q3 THEN NULL::NUMERIC END,
      CASE WHEN s_q3 THEN a3.abi END,
      CASE WHEN s_q3 THEN a3.ai END,
      CASE WHEN s_q3 THEN COALESCE(a3.abi,0)+COALESCE(a3.ai,0) END,
      CASE WHEN s_q3 THEN a3.m1 END,
      CASE WHEN s_q3 THEN a3.m2 END,
      CASE WHEN s_q3 THEN a3.m3 END,
      CASE WHEN s_q3 THEN COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0) END,
      CASE WHEN s_q3 THEN ROUND((COALESCE(a3.abi,0)+COALESCE(a3.ai,0)) -
                                (COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)), 1) END,
      -- Q4
      CASE WHEN s_q4 THEN NULL::NUMERIC END,
      CASE WHEN s_q4 THEN a4.abi END,
      CASE WHEN s_q4 THEN a4.ai END,
      CASE WHEN s_q4 THEN COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
      CASE WHEN s_q4 THEN a4.m1 END,
      CASE WHEN s_q4 THEN a4.m2 END,
      CASE WHEN s_q4 THEN a4.m3 END,
      CASE WHEN s_q4 THEN a4.spod END,
      CASE WHEN s_q4 THEN COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
      CASE WHEN s_q4 THEN ROUND((COALESCE(a4.abi,0)+COALESCE(a4.ai,0)) -
                                (COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END,
      -- year
      CASE WHEN s_year THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+
                            COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
                            COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+
                            COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
      CASE WHEN s_year THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
                            COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
                            COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
                            COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
      CASE WHEN s_year THEN
        ROUND(
          (COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
           COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0))
          -
          (COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
           COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
           COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
           COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)),
          1)
      END,
      -- план-блоки (rollup)
      CASE WHEN s_q1 THEN a1.bp END,
      CASE WHEN s_q2 THEN a2.bp END,
      CASE WHEN s_q3 THEN a3.bp END,
      CASE WHEN s_q4 THEN a4.bp END,
      CASE WHEN s_year THEN COALESCE(a1.bp,0)+COALESCE(a2.bp,0)+
                           COALESCE(a3.bp,0)+COALESCE(a4.bp,0) END,
      CASE WHEN s_q2 THEN a2.cp END,
      CASE WHEN s_q3 THEN a3.cp END,
      CASE WHEN s_q4 THEN a4.cp END,
      t.path
    FROM tw t
    LEFT JOIN tw_aq1 a1 ON a1.id = t.id
    LEFT JOIN tw_aq2 a2 ON a2.id = t.id
    LEFT JOIN tw_aq3 a3 ON a3.id = t.id
    LEFT JOIN tw_aq4 a4 ON a4.id = t.id
  ) sub
  ORDER BY sub._sort_path;

END;
$function$ 
LANGUAGE plpgsql STABLE;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.upd_form3_cell / v3.upd_form3_cells — обёртки FORM_3 с проверкой роли+окна
-- ════════════════════════════════════════════════════════════════════════════
-- Параллель upd_form_cell_authz.sql, но для ветки rf_project_report. Каждая
-- ячейка прогоняется через v3.can_edit3(report_id, column, user_id). Если хоть
-- одна запрещена — RAISE EXCEPTION в формате, совместимом с конвенцией v3:
--   • role_not_allowed: <column>
--   • org_not_assigned: <column>
--   • window_closed: <column> (opens <ISO8601>)   — окно ещё не наступило
--   • window_closed: <column> (closed <ISO8601>)  — окно уже закрыто
--   • window_closed: <column> (no phase)          — фаз вообще нет
--
-- HTTP-слой парсит префикс до `:` и мапит на 403 (см. SPEC.md §5).
--
-- ⚠ Применять СТРОГО после upd_form3_cell.sql (low-level 2-арг/4-арг). Здесь
-- добавляется p_user_id → это ДРУГИЕ сигнатуры (3-арг/5-арг), которые
-- сосуществуют с low-level и делегируют в них после проверки. HTTP вызывает
-- версии с user_id. «Листа» тут нет — report_type определяется отчётом внутри
-- can_edit3.
-- ════════════════════════════════════════════════════════════════════════════

-- ─── upd_form3_cells (массовый, с авторизацией) ────────────────────────────
DROP FUNCTION IF EXISTS v3.upd_form3_cells(INT, JSONB, INT);

CREATE FUNCTION v3.upd_form3_cells(
    p_report_id INT,
    p_changes   JSONB,
    p_user_id   INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
    v_change   JSONB;
    v_column   TEXT;
    v_decision JSONB;
    v_msg      TEXT;
BEGIN
    IF p_user_id IS NULL THEN
        RAISE EXCEPTION 'role_not_allowed: <missing user_id>';
    END IF;

    IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN
        RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id;
    END IF;

    IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
        RAISE EXCEPTION 'p_changes must be a JSONB array';
    END IF;

    -- Проверяем КАЖДУЮ ячейку до начала записи. Падаем на первой запрещённой —
    -- транзакция целиком откатится, частичной записи не будет.
    FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
    LOOP
        v_column := v_change->>'column';
        IF v_column IS NULL THEN
            RAISE EXCEPTION 'change must have column: %', v_change;
        END IF;

        v_decision := v3.can_edit3(p_report_id, v_column, p_user_id);

        IF NOT (v_decision->>'ok')::boolean THEN
            IF v_decision->>'code' = 'role_not_allowed' THEN
                RAISE EXCEPTION 'role_not_allowed: %', v_column;

            ELSIF v_decision->>'code' = 'org_not_assigned' THEN
                RAISE EXCEPTION 'org_not_assigned: %', v_column;

            ELSIF v_decision->>'code' = 'window_closed' THEN
                IF v_decision ? 'opens_at' THEN
                    v_msg := format('window_closed: %s (opens %s)',
                                    v_column, v_decision->>'opens_at');
                ELSIF v_decision->>'closes_at' IS NOT NULL THEN
                    v_msg := format('window_closed: %s (closed %s)',
                                    v_column, v_decision->>'closes_at');
                ELSE
                    v_msg := format('window_closed: %s (no phase)', v_column);
                END IF;
                RAISE EXCEPTION '%', v_msg;

            ELSE
                RAISE EXCEPTION '%: %', v_decision->>'code', v_column;
            END IF;
        END IF;
    END LOOP;

    -- Все ячейки разрешены — делегируем в low-level (2-арг). ROW_UPDATE не
    -- логируем (см. logging_events.md): cell-edit'ы слишком частые.
    RETURN QUERY
    SELECT * FROM v3.upd_form3_cells(p_report_id, p_changes);
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

-- ─── upd_form3_cell (одна ячейка, тонкий wrapper) ──────────────────────────
DROP FUNCTION IF EXISTS v3.upd_form3_cell(INT, INT, TEXT, JSONB, INT);

CREATE FUNCTION v3.upd_form3_cell(
    p_report_id INT,
    p_line_id   INT,
    p_column    TEXT,
    p_value     JSONB,
    p_user_id   INT
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
    SELECT * FROM v3.upd_form3_cells(
        p_report_id,
        jsonb_build_array(jsonb_build_object(
            'line_id', p_line_id,
            'column',  p_column,
            'value',   p_value
        )),
        p_user_id
    );
$function$ 
LANGUAGE sql VOLATILE;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.migrate_form3_v2 — DDL под переработку FORM_3 (листы ЛП/ТР, v2)
-- ════════════════════════════════════════════════════════════════════════════
-- Источник: «Пример_Форма_3_new(1).xlsm» (листы шаблон_ЛП / шаблон_ТР) + PDF-дифф
-- аналитика. Финансовая часть переразбита с 4 на 10 блоков и переведена на
-- многолетнюю раскладку (год1/год2/год3 рядом, кварталы повторяются на год).
--
-- Новые блоки внутри одного года:
--   • Базовый план по Лимиту Правления   — ручной ввод q1–q4, Год = Σ кв.
--   • Скоррект план по Лимиту Правления   — ручной ввод q2–q4 (без q1, без Года)
--   • Фактические затраты                 — уже покрыты rf_project_report_quarter
--     (adj_by_items / adj_increase / actual_m1..m3 / actual_spod), «Остаток» и
--     квартальные/годовые итоги считаются на лету в read-функции.
--
-- Многолетность уже поддержана моделью: один project → много rf_project_report
-- по (year × report_type). Здесь добавляется ТОЛЬКО хранение план-блоков и
-- «Системный код ВСП» в шапке проекта.
-- ════════════════════════════════════════════════════════════════════════════

-- ───────────── 1. rf_project_report_quarter: план-блоки ─────────────────────
-- base_plan      — «Базовый план по Лимиту Правления», поквартально (q1–q4).
-- corrected_plan — «Скоррект план по Лимиту Правления», поквартально (q2–q4).
--                  Для q1 значения не существует (шаблон рисует скорр.план с
--                  II квартала) — гарантируется CHECK ниже + write-слой/RBAC.
ALTER TABLE v3.rf_project_report_quarter
  ADD COLUMN IF NOT EXISTS base_plan      NUMERIC,
  ADD COLUMN IF NOT EXISTS corrected_plan NUMERIC;

-- Скоррект план не заводится в I квартале (нет колонки в шаблоне).
ALTER TABLE v3.rf_project_report_quarter
  DROP CONSTRAINT IF EXISTS chk_v3_rf_quarter_corr_plan_q1;
ALTER TABLE v3.rf_project_report_quarter
  ADD CONSTRAINT chk_v3_rf_quarter_corr_plan_q1
    CHECK (quarter <> 1 OR corrected_plan IS NULL);

-- ───────────── 2. v3.project: «Системный код ВСП» ───────────────────────────
-- Новое поле шапки «Информация о проекте ВСП» (в примере — 1808). Выбор из
-- справочника ИНФО / выпадающего списка; хранится как атрибут проекта (вводится
-- один раз, не дублируется между треками/годами — как и остальные атрибуты ВСП).
ALTER TABLE v3.project
  ADD COLUMN IF NOT EXISTS system_code VARCHAR;

-- ───────────── 3. Обновление энум-списков шапки ────────────────────────────
-- В новом шаблоне списки изменились относительно старого VBA-набора.
-- project_type: новый перечень из 8 значений (шаблон_ЛП F2:F9).
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_project_type_check;
ALTER TABLE v3.project
  ADD CONSTRAINT project_project_type_check
    CHECK (project_type IS NULL OR project_type IN (
      'Открытие ВСП','Реновация ВСП','Закрытие ВСП',
      'Реновация РФ','Закрытие РФ',
      'Открытие УРМ','Реновация УРМ','Закрытие УРМ'
    ));

-- vsp_format («Новый формат ВСП») — поле автозаполняется из справочника INFO
-- («Утвержденный формат»), не вводится вручную и его словарь ведётся как
-- reference-data. Жёсткий CHECK снимаем, чтобы новые форматы не ломали вставку.
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_vsp_format_check;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.v_form3_report_jsonb — JSONB-обёртка над v_form3_report_sections
-- ════════════════════════════════════════════════════════════════════════════
-- Один трек FORM_3 (LIMIT либо CURRENT_EXPENSES) как (row_type, depth,
-- sort_order, data jsonb) — общий формат UI с FORM_1/2/4.
-- ════════════════════════════════════════════════════════════════════════════

DROP FUNCTION IF EXISTS v3.v_form3_report_jsonb(INT, TEXT[]);

CREATE FUNCTION v3.v_form3_report_jsonb(
    p_report_id INT,
    p_sections  TEXT[] DEFAULT NULL
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
  SELECT
    s.row_type,
    s.depth,
    ROW_NUMBER() OVER (ORDER BY s._sort_path) AS sort_order,
    jsonb_build_object(
      'line_id', s.line_id,
      'header', jsonb_build_object(
        'section_code',    s.col_section_code,
        'item_id',         s.col_item_id,
        'num_group_id',    s.col_num_group_id,
        'name',            s.col_name,
        -- expense_item_id из _sort_path: для INPUT путь = {...ei_id, line_id},
        -- для иерархии = {...ei_id} → последний/предпоследний элемент.
        'expense_item_id', CASE WHEN s.row_type = 'INPUT'
                             THEN s._sort_path[array_upper(s._sort_path,1)-1]
                             ELSE s._sort_path[array_upper(s._sort_path,1)] END
      ),
      'q1', jsonb_build_object(
        'base_plan',      s.col_q1_base_plan,
        'adj_by_items',   s.col_q1_adj_by_items,
        'adj_increase',   s.col_q1_adj_increase,
        'total_corr',     s.col_q1_total_corr,
        'm1',             s.col_q1_m1,
        'm2',             s.col_q1_m2,
        'm3',             s.col_q1_m3,
        'quarter_actual', s.col_q1_quarter_actual,
        'economy',        s.col_q1_economy   -- «Остаток» = корр+увелич−квартал
      ),
      'q2', jsonb_build_object(
        'base_plan',      s.col_q2_base_plan,
        'corrected_plan', s.col_q2_corrected_plan,
        'carryover',      s.col_q2_carryover,
        'adj_by_items',   s.col_q2_adj_by_items,
        'adj_increase',   s.col_q2_adj_increase,
        'total_corr',     s.col_q2_total_corr,
        'm1',             s.col_q2_m1,
        'm2',             s.col_q2_m2,
        'm3',             s.col_q2_m3,
        'quarter_actual', s.col_q2_quarter_actual,
        'economy',        s.col_q2_economy
      ),
      'q3', jsonb_build_object(
        'base_plan',      s.col_q3_base_plan,
        'corrected_plan', s.col_q3_corrected_plan,
        'carryover',      s.col_q3_carryover,
        'adj_by_items',   s.col_q3_adj_by_items,
        'adj_increase',   s.col_q3_adj_increase,
        'total_corr',     s.col_q3_total_corr,
        'm1',             s.col_q3_m1,
        'm2',             s.col_q3_m2,
        'm3',             s.col_q3_m3,
        'quarter_actual', s.col_q3_quarter_actual,
        'economy',        s.col_q3_economy
      ),
      'q4', jsonb_build_object(
        'base_plan',      s.col_q4_base_plan,
        'corrected_plan', s.col_q4_corrected_plan,
        'carryover',      s.col_q4_carryover,
        'adj_by_items',   s.col_q4_adj_by_items,
        'adj_increase',   s.col_q4_adj_increase,
        'total_corr',     s.col_q4_total_corr,
        'm1',             s.col_q4_m1,
        'm2',             s.col_q4_m2,
        'm3',             s.col_q4_m3,
        'spod',           s.col_q4_spod,
        'quarter_actual', s.col_q4_quarter_actual,
        'economy',        s.col_q4_economy
      ),
      'totals', jsonb_build_object(
        'base_plan',    s.col_year_base_plan,   -- Год Базового плана (Σ q1–q4)
        'total_corr',   s.col_year_total_corr,
        'total_actual', s.col_year_total_actual,
        'economy',      s.col_year_economy
      )
    ) AS data
  FROM v3.v_form3_report_sections(p_report_id, p_sections) s
  ORDER BY s._sort_path;
$function$ 
LANGUAGE sql STABLE;
-- ════════════════════════════════════════════════════════════════════════════
-- 09_backfill_existing.sql — донакат на СУЩЕСТВУЮЩИЕ отчёты FORM_3
-- ════════════════════════════════════════════════════════════════════════════
-- Нужен ТОЛЬКО если в БД уже есть отчёты rf_project_report, созданные до этого
-- апдейта. На чистой БД / для новых проектов не требуется (add_project и
-- add_project_year уже дают правильные снапшоты и сетку).
--
-- 1) copy_template_to_report использует ON CONFLICT DO NOTHING → у старых
--    отчётов в form3_phase остаются СТАРЫЕ column_keys (без base_plan/
--    corrected_plan). Ресинкаем column_keys из обновлённого phase_template.
-- 2) (ОПЦИОНАЛЬНО) досеиваем фиксированную сетку статей для старых отчётов.
--    ⚠ Меняет набор строк старых отчётов (добавит все листовые статьи FORM_3).
--    Раскомментируйте, только если хотите привести старые отчёты к фикс-сетке.
-- ════════════════════════════════════════════════════════════════════════════


-- 1. Ресинк column_keys существующих снапшотов с шаблоном (только добавляет
--    новые ключи план-блоков; окна opens_at/closes_at не трогаем).
UPDATE v3.form3_phase fp
   SET column_keys = pt.column_keys
  FROM v3.rf_project_report r,
       v3.phase_template pt
 WHERE fp.rf_project_report_id = r.id
   AND pt.form_type  = 'FORM_3'
   AND pt.sheet      = r.report_type
   AND pt.phase_code = fp.phase_code
   AND pt.role       = fp.role
   AND fp.column_keys IS DISTINCT FROM pt.column_keys;

-- Также подхватить фазы, которых у старого отчёта вовсе не было (идемпотентно).
SELECT v3.copy_template_to_report(id) FROM v3.rf_project_report;

-- 2. ОПЦИОНАЛЬНО: фиксированная сетка статей для старых отчётов.
--    Раскомментируйте при необходимости:
-- SELECT v3.seed_form3_report_lines(id) FROM v3.rf_project_report;

-- ════════════════════════════════════════════════════════════════════════════
-- v3.v_form3_project_jsonb — многолетний широкий лист ЛП/ТР одного проекта
-- ════════════════════════════════════════════════════════════════════════════
-- Новый шаблон FORM_3 (v2) показывает годы проекта рядом (год1|год2|…) —
-- «кварталы повторяются для каждого года проекта». Горизонт переменный: сколько
-- у проекта заведено годовых rf_project_report того же трека, столько year-групп.
--
-- Модель: каждый (год × трек) — отдельный rf_project_report со своим набором
-- строк (фиксированная сетка статей, одинаковая во всех годах). Здесь мы сшиваем
-- эти годовые отчёты по СТАТЬЕ (expense_item_id — стабильный ключ между годами;
-- line_id у строки в каждом году свой и лежит внутри года — для адресной правки).
--
-- Возврат — общий формат UI (row_type, depth, sort_order, data), где data:
--   { "header": {...},                       -- общая шапка строки (одна на все годы)
--     "years":  { "2025": <год-объект>, ... },-- год → data из v_form3_report_jsonb
--                                              --   (внутри line_id года + q1..q4 + totals)
--     "project_total": <NUMERIC> }            -- «Стоимость проекта за N лет» =
--                                              --   Σ годовых фактических итогов (totals.total_actual)
--
-- p_report_type — 'LIMIT' (ЛП) либо 'CURRENT_EXPENSES' (ТР).
-- p_sections — как в v_form3_report_sections ('q1'..'q4','year'), прокидывается
-- в каждый год.
-- ════════════════════════════════════════════════════════════════════════════

DROP FUNCTION IF EXISTS v3.v_form3_project_jsonb(INT, VARCHAR, TEXT[]);

CREATE FUNCTION v3.v_form3_project_jsonb(
    p_project_id  INT,
    p_report_type VARCHAR,
    p_sections    TEXT[] DEFAULT NULL
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
  WITH reports AS (
    SELECT r.id AS report_id, r.year
      FROM v3.rf_project_report r
     WHERE r.project_id = p_project_id
       AND r.report_type = p_report_type
  ),
  -- Разворачиваем каждый год в строки его листа
  per_year AS (
    SELECT
      v.row_type,
      v.depth,
      (v.data->'header'->>'expense_item_id')::INT AS eiid,
      v.data->'header'                            AS header,
      v.sort_order                                AS y_sort,
      rp.year                                     AS year,
      v.data                                      AS ydata
    FROM reports rp
    CROSS JOIN LATERAL v3.v_form3_report_jsonb(rp.report_id, p_sections) v
  ),
  -- Сшиваем по идентичности строки (row_type + статья): сетка одинакова во всех годах
  agg AS (
    SELECT
      py.row_type,
      py.depth,
      py.eiid,
      MIN(py.y_sort)                                       AS sort_order,
      (array_agg(py.header ORDER BY py.year))[1]           AS header,
      jsonb_object_agg(py.year::text, py.ydata ORDER BY py.year) AS years,
      SUM((py.ydata->'totals'->>'total_actual')::numeric)  AS project_total
    FROM per_year py
    GROUP BY py.row_type, py.depth, py.eiid
  )
  SELECT
    a.row_type,
    a.depth,
    ROW_NUMBER() OVER (ORDER BY a.sort_order) AS sort_order,
    jsonb_build_object(
      'header',        a.header,
      'years',         a.years,
      'project_total', a.project_total
    ) AS data
  FROM agg a
  ORDER BY a.sort_order;
$function$ 
LANGUAGE sql STABLE;
-- ════════════════════════════════════════════════════════════════════════════
-- v3.phase_template — seed (фазы редактирования: роль × лист × окно × колонки)
-- ════════════════════════════════════════════════════════════════════════════
-- Шаблон редактируется админом. Изменения здесь НЕ влияют на уже созданные
-- формы — у них свой snapshot в v3.form_phase.
--
-- Этот seed — стартовая разметка, в которой жёстко прошита только семантика
-- sequestration / seq_ssp / seq_dfip (где actor однозначно следует из v3-кода).
-- Остальные колонки (plan, q1.adj_*, contract_*, allocation, reserve, ckk,
-- collegial, header.*, ...) НЕ покрыты — добавлять по мере согласования
-- бизнес-правила «кто правит в какую фазу».
--
-- Для smoke-теста все фазы открыты широко (весь 2026). В проде окна должны
-- соответствовать реальному регламенту бюджетного цикла.
--
-- Возвратные фазы (EXECUTOR_RF→DFIP→EXECUTOR_RF) — несколько строк с одной
-- ролью, разные phase_code и окна, без особых случаев.
--
-- ⚠ role ссылается на v3.role(code): ADMIN / DFIP / EXECUTOR_RF. ADMIN в
-- шаблоне быть не должен (в can_edit делается bypass) — CHECK-констрейнт
-- chk_v3_phase_template_no_admin отрежет.
-- ════════════════════════════════════════════════════════════════════════════

TRUNCATE TABLE v3.phase_template;

-- ─── FORM_1: actor sequestration = DFIP (alias на одно поле в v3.sequestration)
INSERT INTO v3.phase_template
    (form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
VALUES
    ('FORM_1', 'AHR',  'DFIP_SEQ_2026',  'DFIP',
     ARRAY['sequestration.q1','sequestration.q2','sequestration.q3','sequestration.q4',
           'sequestration.justification'],
     '2026-01-01 00:00:00+00', '2026-12-31 23:59:59+00'),
    ('FORM_1', 'CAP',  'DFIP_SEQ_2026',  'DFIP',
     ARRAY['sequestration.q1','sequestration.q2','sequestration.q3','sequestration.q4',
           'sequestration.justification'],
     '2026-01-01 00:00:00+00', '2026-12-31 23:59:59+00'),
    ('FORM_1', 'OPER', 'DFIP_SEQ_2026',  'DFIP',
     ARRAY['sequestration.q1','sequestration.q2','sequestration.q3','sequestration.q4',
           'sequestration.justification'],
     '2026-01-01 00:00:00+00', '2026-12-31 23:59:59+00');

-- ─── FORM_2 / FORM_4: разнесены по actor через scope (seq_ssp / seq_dfip)
INSERT INTO v3.phase_template
    (form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
SELECT ft, sh, ph, rl, ck,
       '2026-01-01 00:00:00+00'::timestamptz,
       '2026-12-31 23:59:59+00'::timestamptz
FROM (VALUES
    ('FORM_2','AHR'),  ('FORM_2','CAP'),  ('FORM_2','OPER'),
    ('FORM_4','AHR'),  ('FORM_4','CAP'),  ('FORM_4','OPER')
) AS f(ft, sh)
CROSS JOIN (VALUES
    ('SSP_SEQ_2026',  'EXECUTOR_RF', ARRAY['seq_ssp.q1','seq_ssp.q2','seq_ssp.q3','seq_ssp.q4',
                                           'seq_ssp.justification']),
    ('DFIP_SEQ_2026', 'DFIP',        ARRAY['seq_dfip.q1','seq_dfip.q2','seq_dfip.q3','seq_dfip.q4',
                                           'seq_dfip.justification'])
) AS p(ph, rl, ck);

-- ─── FORM_3: sheet = report_type ('LIMIT' / 'CURRENT_EXPENSES') ────────────
-- Ветка rf_project_report. column_keys — scope.field формата qN.<field>
-- (см. upd_form3_cell.sql). Год несёт report_id (каждый год проекта — свой
-- rf_project_report), поэтому ключи БЕЗ префикса года. Наборы полей:
--   Факт:          adj_by_items / adj_increase / m1 / m2 / m3 по кварталу + q4.spod
--   Базовый план:  q1..q4.base_plan
--   Скоррект план: q2..q4.corrected_plan (без q1 — шаблон с II квартала)
-- EXECUTOR_RF — основной заполнитель отчётов РФ. Стартовая разметка: одна
-- широкая фаза заполнения на весь 2026 для обоих треков. Реальные окна/роли
-- (в т.ч. проверка DFIP) — уточнять по регламенту.
INSERT INTO v3.phase_template
    (form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
SELECT 'FORM_3', sh, 'RF_FILL_2026', 'EXECUTOR_RF',
       ARRAY[
         -- Факт
         'q1.adj_by_items','q1.adj_increase','q1.m1','q1.m2','q1.m3',
         'q2.adj_by_items','q2.adj_increase','q2.m1','q2.m2','q2.m3',
         'q3.adj_by_items','q3.adj_increase','q3.m1','q3.m2','q3.m3',
         'q4.adj_by_items','q4.adj_increase','q4.m1','q4.m2','q4.m3','q4.spod',
         -- Базовый план (q1–q4)
         'q1.base_plan','q2.base_plan','q3.base_plan','q4.base_plan',
         -- Скоррект план (q2–q4)
         'q2.corrected_plan','q3.corrected_plan','q4.corrected_plan'
       ],
       '2026-01-01 00:00:00+00'::timestamptz,
       '2026-12-31 23:59:59+00'::timestamptz
FROM (VALUES ('LIMIT'), ('CURRENT_EXPENSES')) AS s(sh);

-- Образец возвратной DFIP-фазы (ГО правит корректировки после РФ):
-- INSERT INTO v3.phase_template
--     (form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
-- VALUES ('FORM_3', 'CURRENT_EXPENSES', 'DFIP_REVIEW_2026', 'DFIP',
--     ARRAY['q1.adj_by_items','q1.adj_increase','q2.adj_by_items','q2.adj_increase'],
--     '2026-04-01 00:00:00+00', '2026-04-30 23:59:59+00');

-- ─── TODO: phase для plan, q1.adj_*, contract, allocation, reserve, etc. ───
-- Образец на будущее — план EXECUTOR_RF в первое окно цикла:
--
-- INSERT INTO v3.phase_template
--     (form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
-- VALUES (
--     'FORM_1', 'AHR', 'SSP_PLAN_INITIAL_2026', 'EXECUTOR_RF',
--     ARRAY['plan.q1','plan.q2','plan.q3','plan.q4','plan.comment'],
--     '2026-02-01 00:00:00+00', '2026-02-28 23:59:59+00'
-- );
--
-- Полный список scope.field — см. v3/upd_form_cell.sql (комментарий вверху
-- файла + блоки в _apply_form_cell).
