-- ============================================================================
-- Core functions for Zabbix partitioning (Create, Drop, Maintain).
-- ============================================================================

-- Function to check if a partition exists in a specific schema
CREATE OR REPLACE FUNCTION partitions.partition_exists(p_partition_name text, p_schema text) 
RETURNS boolean AS $$
BEGIN
    RETURN EXISTS (
        SELECT 1 FROM pg_class c
        JOIN pg_namespace n ON n.oid = c.relnamespace
        WHERE c.relname = p_partition_name
          AND n.nspname = p_schema
    );
END;
$$ LANGUAGE plpgsql;

-- Function to create a partition
CREATE OR REPLACE PROCEDURE partitions.create_partition(
    p_parent_table text,
    p_start_time timestamp with time zone,
    p_end_time timestamp with time zone,
    p_period text
) LANGUAGE plpgsql AS $$
DECLARE
    v_partition_name text;
    v_start_ts bigint;
    v_end_ts bigint;
    v_suffix text;
    v_parent_schema text;
BEGIN
    -- Determine the schema of the parent table
    SELECT n.nspname INTO v_parent_schema
    FROM pg_class c
    JOIN pg_namespace n ON n.oid = c.relnamespace
    WHERE c.relname = p_parent_table AND pg_table_is_visible(c.oid);

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Parent table % not found', p_parent_table;
    END IF;
    -- (No changes needed for time here as passed params are already UTC-adjusted in caller)
    v_start_ts := extract(epoch from p_start_time)::bigint;
    v_end_ts := extract(epoch from p_end_time)::bigint;
    
    IF p_period = 'month' THEN
        v_suffix := to_char(p_start_time, 'YYYYMM');
    ELSIF p_period LIKE '%hour%' THEN
        v_suffix := to_char(p_start_time, 'YYYYMMDDHH24');
    ELSE
        v_suffix := to_char(p_start_time, 'YYYYMMDD');
    END IF;
    
    v_partition_name := p_parent_table || '_p' || v_suffix;

    IF NOT partitions.partition_exists(v_partition_name, v_parent_schema) THEN
        BEGIN
            EXECUTE format(
                'CREATE TABLE %I.%I PARTITION OF %I.%I FOR VALUES FROM (%s) TO (%s)',
                v_parent_schema, v_partition_name, v_parent_schema, p_parent_table, v_start_ts, v_end_ts
            );
        EXCEPTION
            WHEN invalid_object_definition THEN
                -- Ignore overlap errors (e.g., when transitioning from daily to hourly partitioning)
                RAISE NOTICE 'Partition % overlaps with an existing partition. Skipping.', v_partition_name;
            WHEN duplicate_table THEN
                -- Ignore race condition: another process created the partition concurrently
                RAISE NOTICE 'Partition % already exists (concurrent creation). Skipping.', v_partition_name;
        END;
    END IF;
END;
$$;

-- Function to drop old partitions
CREATE OR REPLACE PROCEDURE partitions.drop_old_partitions(
    p_parent_table text,
    p_retention interval,
    p_period text
) LANGUAGE plpgsql AS $$
DECLARE
    v_cutoff_ts bigint;
    v_partition record;
    v_partition_date timestamp with time zone;
    v_suffix text;
    v_partition_schema text;
BEGIN
    -- Calculate cutoff timestamp
    v_cutoff_ts := extract(epoch from (now() - p_retention))::bigint;

    FOR v_partition IN 
        SELECT 
            child.relname AS partition_name,
            n.nspname AS partition_schema
        FROM pg_inherits
        JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
        JOIN pg_class child ON pg_inherits.inhrelid = child.oid
        JOIN pg_namespace n ON child.relnamespace = n.oid
        WHERE parent.relname = p_parent_table AND pg_table_is_visible(parent.oid)
    LOOP
        -- Parse partition suffix to determine age
        -- Format: parent_pYYYYMM or parent_pYYYYMMDD
        v_suffix := substring(v_partition.partition_name from length(p_parent_table) + 3);
        
        BEGIN
            IF length(v_suffix) = 6 THEN -- YYYYMM
                v_partition_date := timezone('UTC', to_timestamp(v_suffix || '01', 'YYYYMMDD')::timestamp without time zone);
            ELSIF length(v_suffix) = 8 THEN -- YYYYMMDD
                v_partition_date := timezone('UTC', to_timestamp(v_suffix, 'YYYYMMDD')::timestamp without time zone);
            ELSIF length(v_suffix) = 10 THEN -- YYYYMMDDHH
                v_partition_date := timezone('UTC', to_timestamp(v_suffix, 'YYYYMMDDHH24')::timestamp without time zone);
            ELSE
                CONTINUE; -- Ignore non-matching suffix lengths
            END IF;
        EXCEPTION WHEN OTHERS THEN
            -- Safely ignore parsing errors for oddly named partitions
            CONTINUE;
        END;

        -- Now check retention and execute DROP TABLE (so dropping errors are correctly raised!)
        IF length(v_suffix) = 6 THEN -- YYYYMM
            IF extract(epoch from (v_partition_date + '1 month'::interval)) < v_cutoff_ts THEN
                RAISE NOTICE 'Dropping old partition %', v_partition.partition_name;
                EXECUTE format('DROP TABLE %I.%I', v_partition.partition_schema, v_partition.partition_name);
                COMMIT; -- Release lock immediately
            END IF;
        ELSIF length(v_suffix) = 8 THEN -- YYYYMMDD
            -- If period is weekly, the partition spans an entire week. Otherwise, it spans one day.
            IF p_period = 'week' THEN
                IF extract(epoch from (v_partition_date + '1 week'::interval)) < v_cutoff_ts THEN
                    RAISE NOTICE 'Dropping old partition %', v_partition.partition_name;
                    EXECUTE format('DROP TABLE %I.%I', v_partition.partition_schema, v_partition.partition_name);
                    COMMIT; -- Release lock immediately
                END IF;
            ELSE
                IF extract(epoch from (v_partition_date + '1 day'::interval)) < v_cutoff_ts THEN
                    RAISE NOTICE 'Dropping old partition %', v_partition.partition_name;
                    EXECUTE format('DROP TABLE %I.%I', v_partition.partition_schema, v_partition.partition_name);
                    COMMIT; -- Release lock immediately
                END IF;
            END IF;
        ELSIF length(v_suffix) = 10 THEN -- YYYYMMDDHH
            IF extract(epoch from (v_partition_date + p_period::interval)) < v_cutoff_ts THEN
                RAISE NOTICE 'Dropping old partition %', v_partition.partition_name;
                EXECUTE format('DROP TABLE %I.%I', v_partition.partition_schema, v_partition.partition_name);
                COMMIT; -- Release lock immediately
            END IF;
        END IF;
    END LOOP;
END;
$$;

-- MAIN Procedure to maintain a single table
CREATE OR REPLACE PROCEDURE partitions.maintain_table(
    p_table_name text,
    p_period text,
    p_keep_history interval,
    p_future_partitions integer DEFAULT 5
) LANGUAGE plpgsql AS $$
DECLARE
    v_start_time timestamp with time zone;
    v_period_interval interval;
    i integer;
    v_past_iterations integer;
BEGIN
    IF p_period = 'day' THEN
        v_period_interval := '1 day'::interval;
        v_start_time := date_trunc('day', now(), 'UTC');
        -- Calculate how many past days cover the retention period (86400 seconds = 1 day)
        v_past_iterations := ceil(extract(epoch from p_keep_history) / 86400)::integer;
        
    ELSIF p_period = 'week' THEN
        v_period_interval := '1 week'::interval;
        v_start_time := date_trunc('week', now(), 'UTC');
        -- 604800 seconds = 1 week
        v_past_iterations := ceil(extract(epoch from p_keep_history) / 604800)::integer;

    ELSIF p_period = 'month' THEN
        v_period_interval := '1 month'::interval;
        v_start_time := date_trunc('month', now(), 'UTC');
        -- Approximate 30 days per month (2592000 seconds)
        v_past_iterations := ceil(extract(epoch from p_keep_history) / 2592000)::integer;
        
    ELSIF p_period LIKE '%hour%' THEN
        v_period_interval := p_period::interval;
        v_start_time := to_timestamp(floor(extract(epoch from now()) / extract(epoch from v_period_interval)) * extract(epoch from v_period_interval));
        v_past_iterations := ceil(extract(epoch from p_keep_history) / extract(epoch from v_period_interval))::integer;
        
    ELSE
        RAISE EXCEPTION 'Unsupported partitioning period: %', p_period;
    END IF;

    -- 1. Create Future Partitions (Current + Buffer)
    FOR i IN 0..p_future_partitions LOOP
        CALL partitions.create_partition(
            p_table_name,
            v_start_time + (i * v_period_interval),
            v_start_time + ((i + 1) * v_period_interval),
            p_period
        );
        COMMIT; -- Release lock immediately
    END LOOP;
    
    -- 2. Create Past Partitions (Covering retention period)
    IF v_past_iterations > 0 THEN
        FOR i IN 1..v_past_iterations LOOP
            CALL partitions.create_partition(
                p_table_name,
                v_start_time - (i * v_period_interval),
                v_start_time - ((i - 1) * v_period_interval),
                p_period
            );
            COMMIT; -- Release lock immediately
        END LOOP;
    END IF;

    -- 3. Drop Old Partitions
    CALL partitions.drop_old_partitions(p_table_name, p_keep_history, p_period);
    
    -- 4. Update Metadata
    UPDATE partitions.config SET last_updated = now() WHERE table_name = p_table_name;
END;
$$;

-- Global Maintenance Procedure
CREATE OR REPLACE PROCEDURE partitions.run_maintenance() 
LANGUAGE plpgsql AS $$
DECLARE
    v_row record;
BEGIN
    FOR v_row IN SELECT * FROM partitions.config LOOP
        CALL partitions.maintain_table(v_row.table_name, v_row.period, v_row.keep_history, v_row.future_partitions);
    END LOOP;
END;
$$;

-- Trigger function to silently discard housekeeper tasks for partitioned tables
CREATE OR REPLACE FUNCTION partitions.housekeeper_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
    IF EXISTS (SELECT 1 FROM partitions.config WHERE table_name = NEW.tablename) THEN
        RETURN NULL;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
