from alembic import op
import sqlalchemy as sa


revision = "0009"
down_revision = "0008"
branch_labels = None
depends_on = None


def upgrade() -> None:
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id integer)
            RETURNS integer
            LANGUAGE plpgsql
            AS $function$
            DECLARE
                v_report_type VARCHAR;
                v_inserted    INTEGER;
                v_org_unit    INTEGER;
                v_utc_offset VARCHAR(3);
            BEGIN
                SELECT report_type, org_unit_id INTO v_report_type, v_org_unit
                FROM v3.rf_project_report AS rpr 
                    INNER JOIN v3.project AS p ON p.id = rpr.project_id 
                WHERE rpr.id = p_report_id;

                IF NOT FOUND THEN
                    RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
                END IF;

                SELECT utc_offset INTO v_utc_offset
                    FROM v3.org_unit
                    WHERE id = v_org_unit;

                INSERT INTO v3.form3_phase
                    (rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
                SELECT p_report_id, pt.phase_code, pt.role,
                    pt.column_keys, (pt.opens_at::text || v_utc_offset)::timestamptz, 
                        (pt.closes_at::text || v_utc_offset)::timestamptz
                FROM v3.phase_template pt
                WHERE pt.form_type = 'FORM_3'
                AND pt.sheet     = v_report_type
                ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING;

                GET DIAGNOSTICS v_inserted = ROW_COUNT;
                RETURN v_inserted;
            END;
            $function$
            ;
        """
    )
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.add_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone)
            RETURNS v3.form3_phase
            LANGUAGE plpgsql
            AS $function$
                        DECLARE
                            v_phase v3.form3_phase;
                            v_org_unit_id INT;
                            v_org_unit_title VARCHAR;
                            v_year INT;
                            v_task_name TEXT;
                            v_phase_id TEXT;
                            v_utc_offset VARCHAR(3);
                            v_report_type VARCHAR;
                            v_project_id INT;
                        BEGIN
                            SELECT p.org_unit_id, ou.title, rpr.year, ou.utc_offset, rpr.report_type, rpr.project_id 
                            INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id
                            FROM v3.rf_project_report rpr
                            LEFT JOIN v3.project p ON p.id = rpr.project_id
                            LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
                            WHERE rpr.id = p_rf_project_report_id;
                            
                            INSERT INTO v3.form3_phase
                                (rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
                            VALUES (
                                p_rf_project_report_id, 
                                p_phase_code, 
                                p_role, 
                                p_column_keys, 
                                CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
                                CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
                                
                            )
                            RETURNING * INTO v_phase;

                            
                            v_task_name := trim(concat(
                                'FORM_3',
                                CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
                                CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
                            ));
                            v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);

                            PERFORM v3.log_event(
                                'ACCESS_WINDOW_CHANGE', 'ACCESS',
                                jsonb_build_object(
                                    'entity_type', 'project',
                                    'entity_id', v_project_id,
                                    'core_entity_type', 'form3_phase',
                                    'core_entity_id', v_phase_id,
                                    'action', 'create',
                                    'phase_id', v_phase_id,
                                    'task_name', NULLIF(v_task_name, ''),
                                    'report_id', p_rf_project_report_id,
                                    'org_unit_id', v_org_unit_id,
                                    'org_unit_name', v_org_unit_title,
                                    'year', v_year,
                                    'project_id', v_project_id,
                                    'report_type', v_report_type,
                                    'phase_code', p_phase_code,
                                    'role', v_phase.role,
                                    'column_keys', v_phase.column_keys,
                                    'opens_at', v_phase.opens_at,
                                    'closes_at', v_phase.closes_at
                                ),
                                null, null, v_org_unit_id
                            );

                            RETURN v_phase;
                        END;
                        $function$
            ;
        """
    )
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.upd_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone)
            RETURNS v3.form3_phase
            LANGUAGE plpgsql
            AS $function$
                        DECLARE
                            v_old v3.form3_phase;
                            v_new v3.form3_phase;
                            v_is_extend BOOLEAN := FALSE;
                            v_has_non_extend_change BOOLEAN := FALSE;
                            v_has_window_change BOOLEAN := FALSE;
                            v_changes JSONB := '{}'::jsonb;
                            v_org_unit_id INT;
                            v_org_unit_title VARCHAR;
                            v_form_type_code VARCHAR;
                            v_year INT;
                            v_task_name TEXT;
                            v_phase_id TEXT;
                            v_utc_offset VARCHAR(3);
                            v_opens_at timestamptz;
                            v_closes_at timestamptz;
                            v_report_type VARCHAR;
                            v_project_id INT;
                        BEGIN
                            SELECT * INTO v_old
                            FROM v3.form3_phase
                            WHERE rf_project_report_id = p_rf_project_report_id
                            AND phase_code = p_phase_code;

                            IF NOT FOUND THEN
                                RAISE EXCEPTION 'form3_phase не найден (rf_project_report_id=%, phase=%)',
                                p_rf_project_report_id, p_phase_code;
                            END IF;

                            SELECT p.org_unit_id, ou.title, rpr.year, ou.utc_offset, rpr.report_type, rpr.project_id
                            INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id
                            FROM v3.rf_project_report rpr
                            LEFT JOIN v3.project p ON p.id = rpr.project_id
                            LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
                            WHERE rpr.id = p_rf_project_report_id;
                            v_task_name := trim(concat(
                                'FORM_3',
                                CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
                                CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
                            ));

                            IF p_opens_at is not null THEN
                                v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz;
                            END IF;
                            IF p_closes_at is not null THEN
                                v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz;
                            END IF;

                            UPDATE v3.form3_phase
                            SET role        = COALESCE(p_role, role),
                                column_keys = COALESCE(p_column_keys, column_keys),
                                opens_at    = COALESCE(v_opens_at, opens_at),
                                closes_at   = COALESCE(v_closes_at, closes_at)
                            WHERE rf_project_report_id = p_rf_project_report_id
                            AND phase_code = p_phase_code
                            RETURNING * INTO v_new;

                            
                            v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);

                            v_is_extend := v_new.closes_at > v_old.closes_at;
                            v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
                                OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
                                OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
                            v_has_window_change := v_has_non_extend_change
                                OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);

                            IF v_old.role IS DISTINCT FROM v_new.role THEN
                                v_changes := v_changes || jsonb_build_object(
                                    'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
                                );
                            END IF;
                            IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
                                v_changes := v_changes || jsonb_build_object(
                                    'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
                                );
                            END IF;
                            IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
                                v_changes := v_changes || jsonb_build_object(
                                    'opens_at', jsonb_build_object(
                                        'before', v_old.opens_at,
                                        'after', v_new.opens_at
                                    )
                                );
                            END IF;
                            IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
                                v_changes := v_changes || jsonb_build_object(
                                    'closes_at', jsonb_build_object(
                                        'before', v_old.closes_at,
                                        'after', v_new.closes_at
                                    )
                                );
                            END IF;

                            IF v_is_extend THEN
                                PERFORM v3.log_event(
                                    'ACCESS_EXTEND', 'ACCESS',
                                    jsonb_build_object(
                                        'entity_type', 'project',
                                        'entity_id', v_project_id,
                                        'core_entity_type', 'form3_phase',
                                        'core_entity_id', v_phase_id,
                                        'action', 'update',
                                        'phase_id', v_phase_id,
                                        'phase_name', p_phase_code,
                                        'task_name', NULLIF(v_task_name, ''),
                                        'report_id', p_rf_project_report_id,
                                        'report_type', v_report_type,
                                        'project_id', v_project_id,
                                        'org_unit_id', v_org_unit_id,
                                        'org_unit_name', v_org_unit_title,
                                        'year', v_year,
                                        'phase_code', p_phase_code,
                                        'closes_at_before', v_old.closes_at,
                                        'closes_at_after', v_new.closes_at
                                    ),
                                    null, null, v_org_unit_id
                                );
                            END IF;

                            IF v_has_window_change THEN
                                PERFORM v3.log_event(
                                    'ACCESS_WINDOW_CHANGE', 'ACCESS',
                                    jsonb_build_object(
                                        'entity_type', 'project',
                                        'entity_id', v_project_id,
                                        'core_entity_type', 'form3_phase',
                                        'core_entity_id', v_phase_id,
                                        'action', 'update',
                                        'phase_id', v_phase_id,
                                        'phase_name', p_phase_code,
                                        'task_id', p_rf_project_report_id,
                                        'task_name', NULLIF(v_task_name, ''),
                                        'project_id', v_project_id,
                                        'report_type', v_report_type,
                                        'year', v_year,
                                        'org_unit_id', v_org_unit_id,
                                        'org_unit_name', v_org_unit_title,
                                        'phase_code', p_phase_code,
                                        'changes', v_changes
                                    ),
                                    null, null, v_org_unit_id
                                );
                            END IF;

                            RETURN v_new;
                        END;
                        $function$;
        """
    )
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.del_form3_phase(p_rf_project_report_id integer, p_phase_code character varying)
            RETURNS boolean
            LANGUAGE plpgsql
            AS $function$
            DECLARE
                v_old v3.form3_phase;
                v_org_unit_id INT;
                v_org_unit_title VARCHAR;
                v_year INT;
                v_task_name TEXT;
                v_phase_id TEXT;
                v_report_type VARCHAR;
                v_project_id INT;
            BEGIN
                DELETE FROM v3.form3_phase
                WHERE rf_project_report_id = p_rf_project_report_id
                AND phase_code = p_phase_code
                RETURNING * INTO v_old;

                IF NOT FOUND THEN
                    RETURN FALSE;
                END IF;

                SELECT p.org_unit_id, ou.title, rpr.year, rpr.report_type, rpr.project_id
                INTO v_org_unit_id, v_org_unit_title, v_year, v_report_type, v_project_id
                FROM v3.rf_project_report rpr
                LEFT JOIN v3.project p ON p.id = rpr.project_id
                LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
                WHERE rpr.id = p_rf_project_report_id;
                v_task_name := trim(concat(
                    'FORM_3',
                    CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
                    CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
                ));
                v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);

                PERFORM v3.log_event(
                    'ACCESS_WINDOW_CHANGE', 'ACCESS',
                    jsonb_build_object(                        
                        'entity_type', 'project',
                        'entity_id', v_project_id,
                        'core_entity_type', 'form3_phase',
                        'core_entity_id', v_phase_id,
                        'action', 'delete',
                        'phase_id', v_phase_id,
                        'phase_name', p_phase_code,
                        'task_name', NULLIF(v_task_name, ''),
                        'project_id', v_project_id,
                        'year', v_year,
                        'report_type', v_report_type,
                        'org_unit_id', v_org_unit_id,
                        'org_unit_name', v_org_unit_title,
                        'phase_code', p_phase_code,
                        'role', v_old.role,
                        'column_keys', v_old.column_keys,
                        'opens_at', v_old.opens_at,
                        'closes_at', v_old.closes_at
                    ),
                    null, null, v_org_unit_id
                );
                RETURN TRUE;
            END;
            $function$
            ;
        """
    )
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
            RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
            LANGUAGE plpgsql
            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',
                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$
            ;
        """
    )
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.upd_phases_timezones(p_org_unit_id integer, p_old_tz character varying, p_new_tz character varying)
            RETURNS integer
            LANGUAGE plpgsql
            AS $function$
                        DECLARE
                            v_result integer;
                            v_result3 integer;
                        BEGIN
                            IF p_old_tz = p_new_tz THEN
                                RETURN 0;
                            END IF;
                            WITH updated AS (
                                UPDATE v3.form_phase
                                SET opens_at    = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
                                    closes_at   = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
                                WHERE budget_form_id IN (SELECT id FROM v3.budget_form WHERE org_unit_id = p_org_unit_id)
                                AND (
                                        opens_at >= CURRENT_TIMESTAMP
                                        OR closes_at >= CURRENT_TIMESTAMP
                                    )
                                RETURNING *
                            )
                            SELECT COUNT(*) INTO v_result FROM updated;

                            WITH updated AS (
                                UPDATE v3.form3_phase
                                SET opens_at    = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
                                    closes_at   = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
                                WHERE rf_project_report_id IN (
                                    SELECT id FROM V3.rf_project_report rpr WHERE project_id IN (
                                        SELECT id FROM v3.project WHERE org_unit_id = p_org_unit_id
                                    )
                                )
                                AND (
                                        opens_at >= CURRENT_TIMESTAMP
                                        OR closes_at >= CURRENT_TIMESTAMP
                                    )
                                RETURNING *
                            )
                            SELECT COUNT(*) INTO v_result3 FROM updated;
                            RETURN v_result + v_result3;
                            
                        END;
                        $function$
            ;
        """
    )
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone)
            RETURNS v3.form_phase
            LANGUAGE plpgsql
            AS $function$
                        DECLARE
                            v_phase v3.form_phase;
                            v_org_unit_id INT;
                            v_org_unit_title VARCHAR;
                            v_form_type_code VARCHAR;
                            v_year INT;
                            v_task_name TEXT;
                            v_phase_id TEXT;
                            v_utc_offset VARCHAR(3);
                        BEGIN
                            SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
                            INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
                            FROM v3.budget_form bf
                            LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
                            WHERE bf.id = p_budget_form_id;
                            
                            SELECT utc_offset INTO v_utc_offset
                            FROM v3.org_unit
                            WHERE id = v_org_unit_id;
                            
                            INSERT INTO v3.form_phase(
                                budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at
                            )
                            VALUES (
                                p_budget_form_id, 
                                p_sheet, 
                                p_phase_code, 
                                p_role, 
                                p_column_keys, 
                                CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
                                CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
                                
                            )
                            RETURNING * INTO v_phase;

                            
                            v_task_name := trim(concat(
                                COALESCE(v_form_type_code, ''),
                                CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
                                CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
                            ));
                            v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);

                            PERFORM v3.log_event(
                                'ACCESS_WINDOW_CHANGE', 'ACCESS',
                                jsonb_build_object(
                                    'entity_type', 'task',
                                    'entity_id', p_budget_form_id,
                                    'core_entity_type', 'form_phase',
                                    'core_entity_id', v_phase_id,
                                    'action', 'create',
                                    'phase_id', v_phase_id,
                                    'phase_name', p_phase_code,
                                    'task_id', p_budget_form_id,
                                    'form_id', p_budget_form_id,
                                    'budget_form_id', p_budget_form_id,
                                    'org_unit_id', v_org_unit_id,
                                    'org_unit_name', v_org_unit_title,
                                    'sheet', p_sheet,
                                    'phase_code', p_phase_code,
                                    'role', v_phase.role,
                                    'column_keys', v_phase.column_keys,
                                    'opens_at', v_phase.opens_at,
                                    'closes_at', v_phase.closes_at
                                ),
                                p_budget_form_id, null, v_org_unit_id
                            );

                            RETURN v_phase;
                        END;
                        $function$
            ;
        """
    )
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.del_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying)
            RETURNS boolean
            LANGUAGE plpgsql
            AS $function$
            DECLARE
                v_old v3.form_phase;
                v_org_unit_id INT;
                v_org_unit_title VARCHAR;
                v_form_type_code VARCHAR;
                v_year INT;
                v_task_name TEXT;
                v_phase_id TEXT;
            BEGIN
                DELETE FROM v3.form_phase
                WHERE budget_form_id = p_budget_form_id
                AND sheet = p_sheet
                AND phase_code = p_phase_code
                RETURNING * INTO v_old;

                IF NOT FOUND THEN
                    RETURN FALSE;
                END IF;

                SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
                INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
                FROM v3.budget_form bf
                LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
                WHERE bf.id = p_budget_form_id;
                v_task_name := trim(concat(
                    COALESCE(v_form_type_code, ''),
                    CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
                    CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
                ));
                v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);

                PERFORM v3.log_event(
                    'ACCESS_WINDOW_CHANGE', 'ACCESS',
                    jsonb_build_object(
                        'entity_type', 'task',
                        'entity_id', p_budget_form_id,
                        'core_entity_type', 'form_phase',
                        'core_entity_id', v_phase_id,
                        'action', 'delete',
                        'phase_id', v_phase_id,
                        'phase_name', p_phase_code,
                        'task_id', p_budget_form_id,
                        'task_name', NULLIF(v_task_name, ''),
                        'form_id', p_budget_form_id,
                        'budget_form_id', p_budget_form_id,
                        'org_unit_id', v_org_unit_id,
                        'org_unit_name', v_org_unit_title,
                        'sheet', p_sheet,
                        'phase_code', p_phase_code,
                        'role', v_old.role,
                        'column_keys', v_old.column_keys,
                        'opens_at', v_old.opens_at,
                        'closes_at', v_old.closes_at
                    ),
                    p_budget_form_id, null, v_org_unit_id
                );
                RETURN TRUE;
            END;
            $function$
            ;
        """
    )
    op.execute(
        """
            CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone)
            RETURNS v3.form_phase
            LANGUAGE plpgsql
            AS $function$
                        DECLARE
                            v_old v3.form_phase;
                            v_new v3.form_phase;
                            v_is_extend BOOLEAN := FALSE;
                            v_has_non_extend_change BOOLEAN := FALSE;
                            v_has_window_change BOOLEAN := FALSE;
                            v_changes JSONB := '{}'::jsonb;
                            v_org_unit_id INT;
                            v_org_unit_title VARCHAR;
                            v_form_type_code VARCHAR;
                            v_year INT;
                            v_task_name TEXT;
                            v_phase_id TEXT;
                            v_utc_offset VARCHAR(3);
                            v_opens_at timestamptz;
                            v_closes_at timestamptz;
                        BEGIN
                            SELECT * INTO v_old
                            FROM v3.form_phase
                            WHERE budget_form_id = p_budget_form_id
                            AND sheet = p_sheet
                            AND phase_code = p_phase_code;

                            IF NOT FOUND THEN
                                RAISE EXCEPTION 'form_phase не найден (form_id=%, sheet=%, phase=%)',
                                p_budget_form_id, p_sheet, p_phase_code;
                            END IF;

                            SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year, ou.utc_offset
                            INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year, v_utc_offset
                            FROM v3.budget_form bf
                            LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
                            WHERE bf.id = p_budget_form_id;
                            v_task_name := trim(concat(
                                COALESCE(v_form_type_code, ''),
                                CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
                                CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
                            ));

                            IF p_opens_at is not null THEN
                                v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz;
                            END IF;
                            IF p_closes_at is not null THEN
                                v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz;
                            END IF;

                            UPDATE v3.form_phase
                            SET role        = COALESCE(p_role, role),
                                column_keys = COALESCE(p_column_keys, column_keys),
                                opens_at    = COALESCE(v_opens_at, opens_at),
                                closes_at   = COALESCE(v_closes_at, closes_at)
                            WHERE budget_form_id = p_budget_form_id
                            AND sheet = p_sheet
                            AND phase_code = p_phase_code
                            RETURNING * INTO v_new;

                            
                            v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);

                            v_is_extend := v_new.closes_at > v_old.closes_at;
                            v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
                                OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
                                OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
                            v_has_window_change := v_has_non_extend_change
                                OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);

                            IF v_old.role IS DISTINCT FROM v_new.role THEN
                                v_changes := v_changes || jsonb_build_object(
                                    'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
                                );
                            END IF;
                            IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
                                v_changes := v_changes || jsonb_build_object(
                                    'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
                                );
                            END IF;
                            IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
                                v_changes := v_changes || jsonb_build_object(
                                    'opens_at', jsonb_build_object(
                                        'before', v_old.opens_at,
                                        'after', v_new.opens_at
                                    )
                                );
                            END IF;
                            IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
                                v_changes := v_changes || jsonb_build_object(
                                    'closes_at', jsonb_build_object(
                                        'before', v_old.closes_at,
                                        'after', v_new.closes_at
                                    )
                                );
                            END IF;

                            IF v_is_extend THEN
                                PERFORM v3.log_event(
                                    'ACCESS_EXTEND', 'ACCESS',
                                    jsonb_build_object(
                                        'entity_type', 'task',
                                        'entity_id', p_budget_form_id,
                                        'core_entity_type', 'form_phase',
                                        'core_entity_id', v_phase_id,

                                        'phase_id', v_phase_id,
                                        'phase_name', p_phase_code,
                                        'task_id', p_budget_form_id,
                                        'task_name', NULLIF(v_task_name, ''),
                                        'form_id', p_budget_form_id,
                                        'budget_form_id', p_budget_form_id,
                                        'org_unit_id', v_org_unit_id,
                                        'org_unit_name', v_org_unit_title,
                                        'sheet', p_sheet,
                                        'phase_code', p_phase_code,
                                        'closes_at_before', v_old.closes_at,
                                        'closes_at_after', v_new.closes_at
                                    ),
                                    p_budget_form_id
                                );
                            END IF;

                            IF v_has_window_change THEN
                                PERFORM v3.log_event(
                                    'ACCESS_WINDOW_CHANGE', 'ACCESS',
                                    jsonb_build_object(
                                        'entity_type', 'task',
                                        'entity_id', p_budget_form_id,
                                        'core_entity_type', 'form_phase',
                                        'core_entity_id', v_phase_id,
                                        'action', 'update',
                                        'phase_id', v_phase_id,
                                        'phase_name', p_phase_code,
                                        'task_id', p_budget_form_id,
                                        'task_name', NULLIF(v_task_name, ''),
                                        'form_id', p_budget_form_id,
                                        'budget_form_id', p_budget_form_id,
                                        'org_unit_id', v_org_unit_id,
                                        'org_unit_name', v_org_unit_title,
                                        'sheet', p_sheet,
                                        'phase_code', p_phase_code,
                                        'changes', v_changes
                                    ),
                                    p_budget_form_id, null, v_org_unit_id
                                );
                            END IF;

                            RETURN v_new;
                        END;
                        $function$
            ;
        """
    )


def downgrade() -> None:
    pass
