-- ════════════════════════════════════════════════════════════════════════════
-- migrate_form4_project.sql — выделение FORM_4 программ/проектов в отдельную
-- сущность v3.form4_project (перестают делить таблицу с FORM_3 v3.project).
-- ════════════════════════════════════════════════════════════════════════════
-- Мотивация: v3.project исторически был FORM_4-таблицей (program/project +
-- parent_id), FORM_3-редизайн навесил сверху свои атрибуты (project_type,
-- vsp_format, address, area, staff, org_unit_id) и строгий chk_v3_project_name.
-- Два разных концепта под одним словом «project» → разреженные колонки, нет
-- защиты FK, констрейнт вынужден ветвиться по «виду» строки. Разводим.
--
-- Новая таблица v3.form4_project — точный костяк без FORM_3-полей:
--   program  (level='program', parent_id=NULL)
--   project  (level='project',  parent_id=program)
--
-- Дискриминатор FORM_4 vs FORM_3: FORM_3-строки — те, что ссылаются из
-- v3.rf_project_report (add_project всегда создаёт отчёты для КАЖДОЙ строки,
-- включая программы). Всё остальное в v3.project — FORM_4.
--
-- Порядок: после init.sql + всех migrate_form3*/migrate_to_identity (нужна
-- финальная форма v3.project с org_unit_id). Один прогон — copy защищён
-- проверкой на пустоту form4_project.
-- ════════════════════════════════════════════════════════════════════════════

-- ───── 1. Новая таблица ─────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS v3.form4_project (
    id          SERIAL PRIMARY KEY,
    name        VARCHAR NOT NULL,
    level       VARCHAR CHECK (level IN ('program', 'project')),
    section_code VARCHAR,
    parent_id   INTEGER REFERENCES v3.form4_project(id),
    -- org_unit_id INTEGER REFERENCES v3.org_unit(id),
    form_id INTEGER REFERENCES v3.budget_form(id),
    -- project (level='project') обязан иметь родителя-программу; program — нет.
    CONSTRAINT chk_form4_project_parent CHECK (
        (level = 'program' AND parent_id IS NULL)
        OR (level = 'project')
    ),
    CONSTRAINT chk_form4_project_section_code CHECK (
      (level = 'program') = (section_code IS NOT NULL)
    )
);

CREATE INDEX ix_form4_project_section_code 
  ON v3.form4_project(section_code) WHERE level = 'program';
CREATE INDEX IF NOT EXISTS ix_v3_form4_project_parent   ON v3.form4_project(parent_id);
CREATE INDEX IF NOT EXISTS ix_v3_form4_project_form ON v3.form4_project(form_id);

-- ───── 2. Перенос FORM_4-строк из v3.project (с сохранением id) ──────────────
-- FK budget_line.project_id ссылается по id → id сохраняем 1:1.
-- Копируем обе роли (program + project) одним INSERT: self-FK parent_id внутри
-- одного оператора проверяется в конце, порядок строк не важен.
-- DO $migrate$
-- BEGIN
--   IF NOT EXISTS (SELECT 1 FROM v3.form4_project) THEN
--     INSERT INTO v3.form4_project (id, name, level, parent_id, org_unit_id)
--     SELECT p.id, p.name, p.level, p.parent_id, (p.org_unit_id
--       FROM v3.project p
--      WHERE p.id NOT IN (
--        SELECT project_id FROM v3.rf_project_report WHERE project_id IS NOT NULL
--      );

--     -- Синхронизируем sequence, чтобы новые INSERT не конфликтовали по id.
--     PERFORM setval('v3.form4_project_id_seq',
--                    GREATEST((SELECT COALESCE(MAX(id), 0) FROM v3.form4_project), 1));

--     RAISE NOTICE 'form4_project: перенесено % строк из v3.project',
--       (SELECT count(*) FROM v3.form4_project);
--   ELSE
--     RAISE NOTICE 'form4_project уже наполнена — перенос пропущен';
--   END IF;
-- END
-- $migrate$;

-- ───── 3. Перецепка FK budget_line.project_id на новую таблицу ──────────────
ALTER TABLE v3.budget_line DROP CONSTRAINT IF EXISTS budget_line_project_id_fkey;
ALTER TABLE v3.budget_line ADD CONSTRAINT budget_line_project_id_fkey FOREIGN KEY (project_id) REFERENCES v3.form4_project(id);

-- ───── 4. Удаление перенесённых строк из v3.project ─────────────────────────
-- После перецепки budget_line на v3.project больше не ссылается ничего FORM_4.
-- Остаются только FORM_3-строки (referenced by rf_project_report).
DELETE FROM v3.project p
 WHERE p.id NOT IN (
   SELECT project_id FROM v3.rf_project_report WHERE project_id IS NOT NULL
 );

-- ───── 5. Восстановление chk_v3_project_name в строгой форме ─────────────────
-- FORM_4-строки ушли — v3.project теперь чисто FORM_3, правило коротких имён
-- ВСП применимо ко всей таблице (как до form4_write.sql). NOT VALID сохраняем.
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS chk_v3_project_name;
ALTER TABLE v3.project
  ADD CONSTRAINT chk_v3_project_name
    CHECK (name IS NULL OR (
      length(name) <= 30 AND
      name !~ E'[+\\-/\\\\=&* ]'
    )) NOT VALID;


-- ════════════════════════════════════════════════════════════════════════════
-- v3.add_form4_program / v3.add_form4_project — создание программ и проектов
-- FORM_4 в выделенной таблице v3.form4_project (иерархия program → project).
-- ════════════════════════════════════════════════════════════════════════════
-- FORM_4 хранит иерархию отдельно от FORM_3 (v3.project) — см.
-- migrate_form4_project.sql. Костяк:
--   program  (level='program', parent_id=NULL)      → depth=1 (GROUP) в v_form4
--   project  (level='project',  parent_id=program)  → depth=2 (ITEM)  в v_form4
-- Строки бюджета привязываются к проекту через budget_line.project_id.
--
-- Никаких FORM_3-атрибутов / rf_project_report / snapshot фаз — это ветка
-- budget_form. p_org_unit_id опционален (read-path FORM_4 по нему не фильтрует,
-- заполняется для аудита/принадлежности).
--
-- Порядок применения: после migrate_form4_project.sql (нужна таблица
-- v3.form4_project) и audit.sql (нужна v3.log_event).
-- ════════════════════════════════════════════════════════════════════════════

-- ───── 1. add_form4_program ─────────────────────────────────────────────────
DROP FUNCTION IF EXISTS v3.add_form4_program(VARCHAR, INT, VARCHAR, INT);

CREATE FUNCTION v3.add_form4_program(
    p_name         VARCHAR,
    p_form_id  INT,
    p_section_code VARCHAR DEFAULT NULL
)
RETURNS TABLE (
    project_id INT,
    row_type   VARCHAR,
    depth      INT,
    sort_order BIGINT,
    data       JSONB
) AS $function$
#variable_conflict use_column
DECLARE
  v_pid INT;
BEGIN
  IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
    RAISE EXCEPTION 'p_name обязателен';
  END IF;
  IF p_form_id IS NULL THEN
    RAISE EXCEPTION 'p_form_id обязателен';
  END IF;
  IF NOT EXISTS (
    SELECT 1 FROM v3.budget_form bf 
    WHERE bf.form_type_code = 'FORM_4' AND bf.id = p_form_id
  ) THEN
    RAISE EXCEPTION 'Только FORM_4';
  END IF;
  IF p_section_code IS NOT NULL AND NOT EXISTS (
    SELECT 1 FROM v3.expense_item 
    WHERE section_code = p_section_code AND depth = 0
  ) THEN
    RAISE EXCEPTION 'section_code=% не существует в expense_item (depth=0)', p_section_code;
  END IF;

  INSERT INTO v3.form4_project (name, level, parent_id, form_id, section_code)
  VALUES (p_name, 'program', NULL, p_form_id, p_section_code)
  RETURNING id INTO v_pid;

  PERFORM v3.log_event(
    'PROJECT_CREATE', 'PROJECT',
    jsonb_strip_nulls(jsonb_build_object(
      'project_id',  v_pid,
      'name',        p_name,
      'level',       'program',
      'form_id', p_form_id,
      'form_type',   'FORM_4'
    )),
    p_form_id, NULL, NULL
  );

  RETURN QUERY
  SELECT v_pid, v.row_type, v.depth, v.sort_order, v.data
  FROM v3.v_form4_sheet_jsonb(p_form_id, p_section_code, NULL) v
  WHERE (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = v_pid)
      OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = p_section_code)
  ORDER BY v.sort_order;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;

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

CREATE FUNCTION v3.add_form4_project(
    p_name        VARCHAR,
    p_program_id  INT
)
RETURNS TABLE (
    project_id INT,
    row_type   VARCHAR,
    depth      INT,
    sort_order BIGINT,
    data       JSONB
)
AS $function$
#variable_conflict use_column
DECLARE
  v_pid          INT;
  v_section_code VARCHAR;
  v_form_id INT;
BEGIN
  IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
    RAISE EXCEPTION 'p_name обязателен';
  END IF;
  IF p_program_id IS NULL THEN
    RAISE EXCEPTION 'p_program_id обязателен (проект создаётся под программой)';
  END IF;
  IF NOT EXISTS (SELECT 1 FROM v3.form4_project fp
                  WHERE fp.id = p_program_id AND fp.level = 'program') THEN
    RAISE EXCEPTION 'program id=% не существует или не имеет level=program', p_program_id;
  END IF;

  SELECT form_id INTO v_form_id FROM v3.form4_project WHERE id = p_program_id;

  INSERT INTO v3.form4_project (name, level, parent_id, form_id)
  VALUES (p_name, 'project', p_program_id, v_form_id)
  RETURNING id INTO v_pid;

  PERFORM v3.log_event(
    'PROJECT_CREATE', 'PROJECT',
    jsonb_strip_nulls(jsonb_build_object(
      'project_id',  v_pid,
      'name',        p_name,
      'level',       'project',
      'parent_id',   p_program_id,
      'form_id', v_form_id,
      'form_type',   'FORM_4'
    )),
    v_form_id, NULL, NULL
  );

  SELECT fp.section_code INTO v_section_code
  FROM v3.form4_project fp
  WHERE fp.id = p_program_id AND fp.level = 'program';

  IF v_section_code IS NOT NULL THEN
    RETURN QUERY
    SELECT v_pid, v.row_type, v.depth, v.sort_order, v.data
    FROM v3.v_form4_sheet_jsonb(v_form_id, v_section_code, NULL) v
    WHERE (v.row_type = 'ITEM' AND (v.data->'header'->>'project_id')::INT = v_pid)
        OR (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = p_program_id)
        OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = v_section_code)
    ORDER BY v.sort_order;
  END IF;
END;
$function$ 
LANGUAGE plpgsql VOLATILE;


-- ════════════════════════════════════════════════════════════════════════════
-- v3.v_form4_sheet_jsonb — JSONB-выдача FORM_4 для АХР_Р / КВ_Р / Операц
-- ════════════════════════════════════════════════════════════════════════════
-- Иерархия (4 уровня):
--   ROOT     depth=0  — section_code (1.00.0/3.00.0/4.00.0/5.00.0)
--   PROGRAM  depth=1  — v3.form4_project level=program
--   PROJECT  depth=2  — v3.form4_project level=project (parent_id=program)
--   INPUT    depth=3  — budget_line (R-код)
--
-- Финансовые блоки (plan, seq_dfip, approved, booking, q1-q4, totals)
-- агрегируются на уровнях ROOT/PROGRAM/PROJECT через v3.jsonb_numeric_sum.
-- Текстовые поля (counterparty, comment, justification…) на агрегатных
-- уровнях NULL'ятся (jsonb_numeric_sum их игнорирует).
--
-- p_form_id   — id формы FORM_4
-- p_sheet     — 'AHR' / 'CAP' / 'OPER'
-- p_sections  — массив включаемых блоков (как в v_form4_sheet_lines_jsonb)
-- ════════════════════════════════════════════════════════════════════════════

-- ───── 1. Хелпер: рекурсивная сумма численных листьев двух JSONB ──────────
CREATE OR REPLACE FUNCTION v3.jsonb_numeric_sum(a JSONB, b JSONB)
RETURNS JSONB LANGUAGE sql IMMUTABLE AS $function$
  SELECT CASE
    WHEN a IS NULL THEN b
    WHEN b IS NULL THEN a
    WHEN jsonb_typeof(a) = 'number' AND jsonb_typeof(b) = 'number' THEN
      to_jsonb((a)::text::numeric + (b)::text::numeric)
    WHEN jsonb_typeof(a) = 'object' AND jsonb_typeof(b) = 'object' THEN (
      SELECT jsonb_object_agg(k,
        v3.jsonb_numeric_sum(a -> k, b -> k)
      )
      FROM (
        SELECT k FROM jsonb_object_keys(a) k
        UNION
        SELECT k FROM jsonb_object_keys(b) k
      ) keys
    )
    -- одна сторона число/объект, другая нет → берём числовую/объектную
    WHEN jsonb_typeof(a) IN ('number','object') THEN a
    WHEN jsonb_typeof(b) IN ('number','object') THEN b
    ELSE NULL
  END;
$function$;

DROP AGGREGATE IF EXISTS v3.jsonb_sum(JSONB);
CREATE AGGREGATE v3.jsonb_sum(JSONB) (
  SFUNC = v3.jsonb_numeric_sum,
  STYPE = JSONB
);

-- ───── 2. Основная функция ─────────────────────────────────────────────────
CREATE OR REPLACE FUNCTION v3.v_form4_sheet_jsonb(
    p_form_id   INT,
    p_sheet     VARCHAR,
    p_sections  TEXT[] DEFAULT NULL
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
  -- ═══ 1. Базовые INPUT-строки с расширенной иерархией ═══════════════════════
  WITH inp_raw AS (
    SELECT j.*
    FROM v3.v_form4_sheet_lines_jsonb(p_form_id, p_sheet, p_sections) j
    WHERE j.row_type = 'INPUT'
  ),
  inp AS (
    SELECT
      i.*,
      bl.project_id AS line_project_id,

      -- --- разрешение проекта ---
      -- A. bl.project_id → project (level='project')
      -- B. bl.project_id → program (level='program') — тогда проекта нет
      CASE WHEN prj.id IS NOT NULL THEN prj.id END AS prj_id,
      CASE WHEN prj.id IS NOT NULL THEN prj.name END AS prj_name,

      -- --- разрешение программы ---
      -- A. через проект: prog_via_prj
      -- B. напрямую:    prog_direct
      COALESCE(prog_via_prj.id,   prog_direct.id)   AS prog_id,
      COALESCE(prog_via_prj.name, prog_direct.name) AS prog_name,

      -- --- section (depth=0) ---
      sec.id          AS sec_id,
      sec.section_code AS sec_code,
      sec.name        AS sec_name

    FROM inp_raw i
    JOIN v3.budget_line bl ON bl.id = i.line_id

    -- проект (level='project')
    LEFT JOIN v3.form4_project prj
      ON prj.id = bl.project_id AND prj.level = 'project'

    -- программа через проект
    LEFT JOIN v3.form4_project prog_via_prj
      ON prog_via_prj.id = prj.parent_id AND prog_via_prj.level = 'program'

    -- программа напрямую (bl.project_id → program)
    LEFT JOIN v3.form4_project prog_direct
      ON prog_direct.id = bl.project_id AND prog_direct.level = 'program'

    -- раздел depth=0 (через expense_item)
    JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
    LEFT JOIN LATERAL (
      WITH RECURSIVE up AS (
        SELECT id, parent_id, name, section_code, depth
        FROM v3.expense_item WHERE id = ei.id
        UNION ALL
        SELECT e.id, e.parent_id, e.name, e.section_code, e.depth
        FROM v3.expense_item e JOIN up u ON u.parent_id = e.id
      )
      SELECT id, name, section_code FROM up WHERE depth = 0 LIMIT 1
    ) sec ON TRUE
  ),

  -- ═══ 2. Все программы (включая пустые) ═════════════════════════════════════
  all_programs AS (
    SELECT
      fp.id                 AS prog_id,
      fp.name               AS prog_name,
      fp.section_code,
      sec.id                AS sec_id,
      sec.section_code      AS sec_code,
      sec.name              AS sec_name
    FROM v3.form4_project fp
    LEFT JOIN LATERAL (
      SELECT id, section_code, name
      FROM v3.expense_item
      WHERE section_code = fp.section_code AND depth = 0
      LIMIT 1
    ) sec ON TRUE
    WHERE fp.level = 'program'
      AND (p_sections IS NULL OR fp.section_code = ANY(p_sections))
	  AND (fp.section_code = p_sheet) AND fp.form_id = p_form_id
  ),

  -- ═══ 3. Все проекты под программами (включая пустые) ═══════════════════════
  all_projects AS (
    SELECT
      prj.id           AS prj_id,
      prj.name         AS prj_name,
      prj.parent_id    AS prog_id,
      ap.sec_id, ap.sec_code, ap.sec_name
    FROM v3.form4_project prj
    JOIN all_programs ap ON ap.prog_id = prj.parent_id
    WHERE prj.level = 'project' AND (ap.section_code = p_sheet)
  ),

  -- ═══ 4. Агрегаты с финансовыми данными (только из inp) ════════════════════

  -- PROJECT-агрегаты
  prj_agg AS (
    SELECT
      sec_id,      MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
      prog_id,     MAX(prog_name) AS prog_name,
      prj_id,      MAX(prj_name)  AS prj_name,
      v3.jsonb_sum(plan_data)              AS plan_data,
      v3.jsonb_sum(seq_dfip_data)          AS seq_dfip_data,
      v3.jsonb_sum(approved_data)          AS approved_data,
      v3.jsonb_sum(contract_summary_data)  AS contract_summary_data,
      v3.jsonb_sum(allocation_data)        AS allocation_data,
      v3.jsonb_sum(reserve_data)           AS reserve_data,
      v3.jsonb_sum(collegial_data)         AS collegial_data,
      v3.jsonb_sum(ckk_data)               AS ckk_data,
      v3.jsonb_sum(contract_data)          AS contract_data,
      v3.jsonb_sum(booking_data)           AS booking_data,
      v3.jsonb_sum(q1_data)                AS q1_data,
      v3.jsonb_sum(q2_data)                AS q2_data,
      v3.jsonb_sum(q3_data)                AS q3_data,
      v3.jsonb_sum(q4_data)                AS q4_data,
      v3.jsonb_sum(totals_data)            AS totals_data
    FROM inp WHERE prj_id IS NOT NULL
    GROUP BY sec_id, prog_id, prj_id
  ),

  -- PROGRAM-агрегаты
  prog_agg AS (
    SELECT
      sec_id,      MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
      prog_id,     MAX(prog_name) AS prog_name,
      v3.jsonb_sum(plan_data)              AS plan_data,
      v3.jsonb_sum(seq_dfip_data)          AS seq_dfip_data,
      v3.jsonb_sum(approved_data)          AS approved_data,
      v3.jsonb_sum(contract_summary_data)  AS contract_summary_data,
      v3.jsonb_sum(allocation_data)        AS allocation_data,
      v3.jsonb_sum(reserve_data)           AS reserve_data,
      v3.jsonb_sum(collegial_data)         AS collegial_data,
      v3.jsonb_sum(ckk_data)               AS ckk_data,
      v3.jsonb_sum(contract_data)          AS contract_data,
      v3.jsonb_sum(booking_data)           AS booking_data,
      v3.jsonb_sum(q1_data)                AS q1_data,
      v3.jsonb_sum(q2_data)                AS q2_data,
      v3.jsonb_sum(q3_data)                AS q3_data,
      v3.jsonb_sum(q4_data)                AS q4_data,
      v3.jsonb_sum(totals_data)            AS totals_data
    FROM inp WHERE prog_id IS NOT NULL
    GROUP BY sec_id, prog_id
  ),

  -- ROOT-агрегаты
  root_agg AS (
    SELECT
      sec_id,      MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
      v3.jsonb_sum(plan_data)              AS plan_data,
      v3.jsonb_sum(seq_dfip_data)          AS seq_dfip_data,
      v3.jsonb_sum(approved_data)          AS approved_data,
      v3.jsonb_sum(contract_summary_data)  AS contract_summary_data,
      v3.jsonb_sum(allocation_data)        AS allocation_data,
      v3.jsonb_sum(reserve_data)           AS reserve_data,
      v3.jsonb_sum(collegial_data)         AS collegial_data,
      v3.jsonb_sum(ckk_data)               AS ckk_data,
      v3.jsonb_sum(contract_data)          AS contract_data,
      v3.jsonb_sum(booking_data)           AS booking_data,
      v3.jsonb_sum(q1_data)                AS q1_data,
      v3.jsonb_sum(q2_data)                AS q2_data,
      v3.jsonb_sum(q3_data)                AS q3_data,
      v3.jsonb_sum(q4_data)                AS q4_data,
      v3.jsonb_sum(totals_data)            AS totals_data
    FROM inp
    GROUP BY sec_id
  ),

  -- Нулевой JSONB-шаблон (для пустых программ/проектов)
  zero AS (
    SELECT '{}'::jsonb AS z
  ),

  -- ═══ 5. UNION всех уровней ═════════════════════════════════════════════════
  unioned AS (
    -- ROOT — только из агрегатов (не имеет смысла пустой ROOT без строк)
    SELECT
      'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id,
      jsonb_build_object('section_code', sec_code, 'name', sec_name, 'expense_item_id', sec_id) AS header,
      plan_data, seq_dfip_data, approved_data,
      contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
      contract_data, booking_data,
      q1_data, q2_data, q3_data, q4_data, totals_data,
      ARRAY[sec_id, 0, 0, 0]::INT[] AS sort_path
    FROM root_agg

    UNION ALL

    -- GROUP — из агрегатов (программы с бюджетными строками)
    SELECT
      'GROUP', 1, NULL,
      jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
      plan_data, seq_dfip_data, approved_data,
      contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
      contract_data, booking_data,
      q1_data, q2_data, q3_data, q4_data, totals_data,
      ARRAY[sec_id, prog_id, 0, 0]::INT[]
    FROM prog_agg

    UNION ALL

    -- GROUP — пустые программы (нет ни одной budget_line)
    SELECT
      'GROUP', 1, NULL,
      jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
      NULL::jsonb, NULL::jsonb, NULL::jsonb,  -- plan, seq_dfip, approved
      NULL::jsonb, NULL::jsonb, NULL::jsonb,  -- contract_summary, allocation, reserve
      NULL::jsonb, NULL::jsonb,               -- collegial, ckk
      NULL::jsonb, NULL::jsonb,               -- contract, booking
      NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb,  -- q1-q4, totals
      ARRAY[sec_id, prog_id, 0, 0]::INT[]
    FROM all_programs ap
    WHERE NOT EXISTS (
      SELECT 1 FROM v3.budget_line bl
      WHERE bl.budget_form_id = p_form_id
        AND (
          bl.project_id = ap.prog_id                                    -- прямая привязка к программе
          OR bl.project_id IN (SELECT id FROM v3.form4_project pr       -- через проект
                               WHERE pr.parent_id = ap.prog_id AND pr.level = 'project')
        )
    )

    UNION ALL

    -- ITEM — из агрегатов (проекты с бюджетными строками)
    SELECT
      'ITEM', 2, NULL,
      jsonb_build_object('section_code', sec_code, 'name', prj_name,
                         'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
      plan_data, seq_dfip_data, approved_data,
      contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
      contract_data, booking_data,
      q1_data, q2_data, q3_data, q4_data, totals_data,
      ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
    FROM prj_agg

    UNION ALL

    -- ITEM — пустые проекты (нет budget_line)
    SELECT
      'ITEM', 2, NULL,
      jsonb_build_object('section_code', sec_code, 'name', prj_name,
                         'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
      NULL::jsonb, NULL::jsonb, NULL::jsonb,
      NULL::jsonb, NULL::jsonb, NULL::jsonb,
      NULL::jsonb, NULL::jsonb,
      NULL::jsonb, NULL::jsonb,
      NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb,
      ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
    FROM all_projects apr
    WHERE NOT EXISTS (
      SELECT 1 FROM v3.budget_line bl
      WHERE bl.budget_form_id = p_form_id
        AND bl.project_id = apr.prj_id
    )

    UNION ALL

    -- INPUT — все строки бюджета
    SELECT
      'INPUT', 3, line_id,
      header || jsonb_build_object(
        'project_id',   prj_id,   'project_name',   prj_name,
        'program_id',   prog_id,  'program_name',   prog_name
      ),
      plan_data, seq_dfip_data, approved_data,
      contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
      contract_data, booking_data,
      q1_data, q2_data, q3_data, q4_data, totals_data,
      ARRAY[sec_id, COALESCE(prog_id, 999999), COALESCE(prj_id, 999999), line_id]::INT[]
    FROM inp
  )

  SELECT
    u.row_type, u.depth,
    ROW_NUMBER() OVER (ORDER BY u.sort_path) AS sort_order,
    jsonb_build_object(
      'line_id',          u.line_id,
      'header',           u.header,
      'plan',             u.plan_data,
      'seq_dfip',         u.seq_dfip_data,
      'approved',         u.approved_data,
      'contract_summary', u.contract_summary_data,
      'allocation',       u.allocation_data,
      'reserve',          u.reserve_data,
      'collegial',        u.collegial_data,
      'ckk',              u.ckk_data,
      'contract',         u.contract_data,
      'booking',          u.booking_data,
      'q1',               u.q1_data,
      'q2',               u.q2_data,
      'q3',               u.q3_data,
      'q4',               u.q4_data,
      'totals',           u.totals_data
    ) AS data
  FROM unioned u
  ORDER BY u.sort_path;
$function$
LANGUAGE sql STABLE;



-- ════════════════════════════════════════════════════════════════════════════
-- v3.add_budget_line — создать новую INPUT-строку (budget_line) для FORM_1/2/4.
--
-- Способы передать expense_item:
--   путь A — численный id:    p_expense_item_id (берётся из header.expense_item_id)
--   путь B — резолв по коду:  p_sheet + p_item_id [+ p_section_code для коллизий]
--                             [+ p_direction для FORM_1]
--
-- Авто-direction:
--   FORM_2          → 'Support'
--   FORM_4          → 'Development'
--   FORM_1/OPER     → NULL
--   FORM_1/AHR|CAP  → p_direction обязателен (Support|Development)
-- Если p_direction передан и противоречит авто-значению — ошибка.
--
-- Дубликаты на одном expense_item разрешены (UNIQUE не накладывается).
-- vsp_id опционален — для FORM_2 фронт может не знать ВСП на момент
-- создания, дозаполнит позже через upd_form_cell(.., 'header.vsp_id', ..).
--
-- FORM_3 не поддерживается — там строки живут в rf_project_report_line,
-- API будет в add_form3_line.sql (отдельная итерация).
--
-- Возврат: вся иерархия (ROOT/GROUP/ITEM/SUB_ITEM/SECTION/LEAF) + новая
-- INPUT (отфильтрована по новому line_id). Новый id извлекается из
-- (data->>'line_id')::INT первой INPUT-строки.
-- ════════════════════════════════════════════════════════════════════════════

DROP FUNCTION IF EXISTS v3.add_budget_line(INT, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, INT, VARCHAR);
DROP FUNCTION IF EXISTS v3.add_budget_line(INT, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, INT, VARCHAR, VARCHAR, DATE);

CREATE FUNCTION v3.add_budget_line(
    p_form_id          INT,
    p_expense_item_id  INT     DEFAULT NULL,
    p_sheet            VARCHAR DEFAULT NULL,
    p_item_id          VARCHAR DEFAULT NULL,
    p_section_code     VARCHAR DEFAULT NULL,
    p_direction        VARCHAR DEFAULT NULL,
    p_name             VARCHAR DEFAULT NULL,
    p_internal_order   VARCHAR DEFAULT NULL,
    p_vsp_id           INT     DEFAULT NULL,
    p_project_id       INT     DEFAULT NULL,
    p_justification    VARCHAR DEFAULT NULL,
    -- сателлитные листы (AHR_RENT/UTILITY/SECURITY) используют дополнительно:
    p_contract_number  VARCHAR DEFAULT NULL,
    p_contract_end_date DATE   DEFAULT NULL
)
RETURNS TABLE (
    row_type    VARCHAR,
    depth       INT,
    sort_order  BIGINT,
    data        JSONB
)
AS $function$
DECLARE
  v_form_type    VARCHAR;
  v_eid          INT;
  v_sheet        VARCHAR;
  v_auto_dir     VARCHAR;
  v_final_dir    VARCHAR;
  v_new_id       INT;
  v_cnt          INT;
  v_sat_table    TEXT;
  v_sat_eid      INT;
  v_parent_line  INT;
BEGIN
  -- 1. form_id → form_type
  SELECT form_type_code INTO v_form_type FROM v3.budget_form WHERE id = p_form_id;
  IF v_form_type IS NULL THEN
    RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
  END IF;
  IF v_form_type = 'FORM_3' THEN
    RAISE EXCEPTION 'FORM_3 не использует budget_line — см. add_form3_line (TBD)';
  END IF;

  -- ═══ Сателлитные листы (AHR_RENT/UTILITY/SECURITY) ═══════════════════
  -- Фиксированный expense_item_id на лист — UI не передаёт R-код.
  -- p_vsp_id обязателен. Опц.: p_contract_number, p_contract_end_date.
  -- Внутри: находим/создаём родительскую budget_line с фиксированным ei,
  --         затем INSERT в *_detail. Возврат — одна новая LEAF.
  IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
    v_sat_table := CASE p_sheet
      WHEN 'AHR_RENT'     THEN 'rent_detail'
      WHEN 'AHR_UTILITY'  THEN 'utility_detail'
      WHEN 'AHR_SECURITY' THEN 'security_detail'
    END;
    v_sat_eid := CASE p_sheet
      WHEN 'AHR_RENT'     THEN 293   -- R061031001 "Аренда_ОН"
      WHEN 'AHR_UTILITY'  THEN 24    -- 1.05.1.    "Коммунальные услуги"
      WHEN 'AHR_SECURITY' THEN 779   -- R061062001 "Пультовая охр.ОН"
    END;

    IF p_vsp_id IS NULL THEN
      RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet;
    END IF;
    IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
      RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
    END IF;

    -- Находим/создаём родительскую budget_line под фиксированным ei
    SELECT id INTO v_parent_line
      FROM v3.budget_line
     WHERE budget_form_id = p_form_id AND expense_item_id = v_sat_eid
     LIMIT 1;
    IF v_parent_line IS NULL THEN
      INSERT INTO v3.budget_line (budget_form_id, expense_item_id, direction)
      VALUES (
        p_form_id, v_sat_eid,
        CASE v_form_type WHEN 'FORM_2' THEN 'Support' WHEN 'FORM_4' THEN 'Development' ELSE NULL END
      ) RETURNING id INTO v_parent_line;
    END IF;

    -- INSERT в сателлит
    EXECUTE format(
      'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) '
      || 'VALUES ($1, $2, $3, $4) RETURNING id',
      v_sat_table
    ) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date
      INTO v_new_id;

    -- Возврат — одна новая INPUT-строка (отфильтрована по новому id сателлита)
    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form_view(p_form_id, p_sheet, NULL, NULL) v
     WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = v_new_id;
    RETURN;
  END IF;

  -- 2. Резолв expense_item_id
  IF p_expense_item_id IS NOT NULL THEN
    SELECT ei.id, ei.sheet INTO v_eid, v_sheet
      FROM v3.expense_item ei WHERE ei.id = p_expense_item_id;
    IF v_eid IS NULL 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 = v_eid AND form_type_code = v_form_type
    ) THEN
      RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type;
    END IF;
  ELSE
    IF p_sheet IS NULL OR p_item_id IS NULL THEN
      RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)';
    END IF;

    SELECT count(*) INTO v_cnt
      FROM v3.expense_item ei
      JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
     WHERE ei.item_id = p_item_id
       AND ei.sheet   = p_sheet
       AND eift.form_type_code = v_form_type
       AND (p_section_code IS NULL OR ei.section_code = p_section_code)
       AND (p_direction    IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);

    IF v_cnt = 0 THEN
      RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%',
        p_sheet, p_item_id, p_section_code, p_direction, v_form_type;
    END IF;
    IF v_cnt > 1 THEN
      RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую',
        v_cnt, p_sheet, p_item_id, p_section_code, p_direction;
    END IF;

    SELECT ei.id, ei.sheet INTO v_eid, v_sheet
      FROM v3.expense_item ei
      JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
     WHERE ei.item_id = p_item_id
       AND ei.sheet   = p_sheet
       AND eift.form_type_code = v_form_type
       AND (p_section_code IS NULL OR ei.section_code = p_section_code)
       AND (p_direction    IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
  END IF;

  -- 3. Авто-direction (для FORM_1/AHR|CAP — обязателен у юзера)
  v_auto_dir := CASE
    WHEN v_form_type = 'FORM_2' THEN 'Support'
    WHEN v_form_type = 'FORM_4' THEN 'Development'
    WHEN v_form_type = 'FORM_1' AND v_sheet = 'OPER' THEN NULL
    ELSE NULL
  END;

  IF v_form_type = 'FORM_1' AND v_sheet IN ('AHR','CAP') AND p_direction IS NULL THEN
    RAISE EXCEPTION 'p_direction обязателен для FORM_1 / sheet=% (Support|Development)', v_sheet;
  END IF;

  IF v_form_type IN ('FORM_2','FORM_4') AND p_direction IS NOT NULL AND p_direction <> v_auto_dir THEN
    RAISE EXCEPTION 'p_direction=% несовместим с form_type=% (фиксирован=%)',
      p_direction, v_form_type, v_auto_dir;
  END IF;

  IF v_form_type = 'FORM_1' AND v_sheet = 'OPER' AND p_direction IS NOT NULL THEN
    RAISE EXCEPTION 'p_direction должен быть NULL для FORM_1 / sheet=OPER (получено: %)', p_direction;
  END IF;

  v_final_dir := COALESCE(p_direction, v_auto_dir);

  -- 4. Sanity на vsp/project
  IF p_vsp_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
    RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
  END IF;
  IF p_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.form4_project WHERE id = p_project_id) THEN
    RAISE EXCEPTION 'form4_project id=% не существует', p_project_id;
  END IF;

  -- 5. INSERT
  INSERT INTO v3.budget_line (
    budget_form_id, expense_item_id, name, internal_order,
    vsp_id, project_id, direction, justification
  ) VALUES (
    p_form_id, v_eid, p_name, p_internal_order,
    p_vsp_id, p_project_id, v_final_dir, p_justification
  ) RETURNING id INTO v_new_id;

  -- 5а. Аудит: ROW_CREATE
  PERFORM v3.log_event(
    'ROW_CREATE', 'ROW_CREATE',
    jsonb_build_object(
      'sheet',           v_sheet,
      'line_id',         v_new_id,
      'expense_item_id', v_eid,
      'direction',       v_final_dir,
      'name',            p_name,
      'vsp_id',          p_vsp_id,
      'project_id',      p_project_id
    ),
    p_form_id
  );

  -- 6. Возврат: только иерархия по пути нового ei + новая INPUT
  DECLARE v_ei_path INT[];
  BEGIN
    v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
    RETURN QUERY
    SELECT v.row_type, v.depth, v.sort_order, v.data
      FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) 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;
