-- Safe PostgreSQL schema for entitlements database
-- This version checks for existing objects before creating them
-- Run this if you already have some tables created

-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For fast text searches

-- Create IPSW table if not exists (matches main IPSW schema)
CREATE TABLE IF NOT EXISTS ipsws (
    id TEXT PRIMARY KEY,
    name TEXT,
    version TEXT,
    buildid TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at TIMESTAMP WITH TIME ZONE
);

-- Create devices table if not exists (matches main IPSW schema)
CREATE TABLE IF NOT EXISTS devices (
    name TEXT PRIMARY KEY
);

-- Create ipsw_devices many-to-many table if not exists (matches main IPSW schema)
CREATE TABLE IF NOT EXISTS ipsw_devices (
    ipsw_id TEXT NOT NULL REFERENCES ipsws(id),
    device_name TEXT NOT NULL REFERENCES devices(name),
    PRIMARY KEY(ipsw_id, device_name)
);

-- Entitlement keys table - preserves complete key text
CREATE TABLE IF NOT EXISTS entitlement_keys (
    id SERIAL PRIMARY KEY,
    key TEXT NOT NULL UNIQUE -- CRITICAL: Keep as TEXT - keys must be complete/accurate
);

-- Entitlement values table with optimized hash - preserves complete value text  
CREATE TABLE IF NOT EXISTS entitlement_values (
    id SERIAL PRIMARY KEY,
    value TEXT NOT NULL,     -- CRITICAL: Keep as TEXT - values must be complete/accurate
    value_type VARCHAR(10) NOT NULL CHECK (value_type IN ('bool', 'string', 'array', 'dict', 'number')),
    value_hash CHAR(16) NOT NULL UNIQUE -- Shortened hash for uniqueness (99.999% collision safety)
);

-- Reuse existing paths table from main IPSW schema for consistency
CREATE TABLE IF NOT EXISTS paths (
    id SERIAL PRIMARY KEY,
    path TEXT NOT NULL UNIQUE -- CRITICAL: Keep as TEXT - paths must be complete/accurate
);

-- Main entitlements table (significantly reduced size through normalization)
CREATE TABLE IF NOT EXISTS entitlements (
    id BIGSERIAL PRIMARY KEY, -- Keep as BIGINT for large datasets
    ipsw_id TEXT NOT NULL REFERENCES ipsws(id),
    path_id INTEGER NOT NULL REFERENCES paths(id),
    key_id INTEGER NOT NULL REFERENCES entitlement_keys(id),
    value_id INTEGER NOT NULL REFERENCES entitlement_values(id),
    
    -- Unique constraint to prevent duplicates (fixes original duplicate issue)
    UNIQUE(ipsw_id, path_id, key_id, value_id)
);

-- Create indexes if they don't exist
DO $$
BEGIN
    -- For IPSW version lookups
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_ipsws_version') THEN
        CREATE INDEX idx_ipsws_version ON ipsws(version);
    END IF;
    
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_ipsws_buildid') THEN
        CREATE INDEX idx_ipsws_buildid ON ipsws(buildid);
    END IF;
    
    -- For key pattern searches (GIN index for fast ILIKE)
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_keys_search') THEN
        CREATE INDEX idx_keys_search ON entitlement_keys USING gin(key gin_trgm_ops);
    END IF;
    
    -- For path pattern searches (GIN index for fast ILIKE)  
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_paths_search') THEN
        CREATE INDEX idx_paths_search ON paths USING gin(path gin_trgm_ops);
    END IF;
    
    -- For main table lookups (composite indexes for common query patterns)
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_entitlement_ipsw_key') THEN
        CREATE INDEX idx_entitlement_ipsw_key ON entitlements(ipsw_id, key_id);
    END IF;
    
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_entitlement_ipsw_path') THEN
        CREATE INDEX idx_entitlement_ipsw_path ON entitlements(ipsw_id, path_id);
    END IF;
END $$;

-- Drop materialized view if it exists (we'll recreate it)
DROP MATERIALIZED VIEW IF EXISTS entitlements_search CASCADE;

-- Materialized view for ultra-fast searches (refreshed periodically, not per-query)
CREATE MATERIALIZED VIEW entitlements_search AS
SELECT 
    ek.id,
    i.version as ios_version,
    i.buildid as build_id,
    array_agg(DISTINCT d.name ORDER BY d.name) as device_list,
    up.path as file_path,
    uk.key,
    uv.value_type,
    CASE 
        WHEN uv.value_type = 'string' THEN uv.value
        ELSE NULL
    END as string_value,
    CASE 
        WHEN uv.value_type = 'bool' THEN CASE WHEN uv.value = 'true' THEN true ELSE false END
        ELSE NULL
    END as bool_value,
    CASE 
        WHEN uv.value_type = 'number' THEN uv.value::NUMERIC
        ELSE NULL
    END as number_value,
    CASE 
        WHEN uv.value_type = 'array' THEN uv.value
        ELSE NULL
    END as array_value,
    CASE 
        WHEN uv.value_type = 'dict' THEN uv.value
        ELSE NULL
    END as dict_value,
    i.created_at as release_date
FROM entitlements ek
JOIN ipsws i ON i.id = ek.ipsw_id
JOIN entitlement_keys uk ON uk.id = ek.key_id
JOIN entitlement_values uv ON uv.id = ek.value_id
JOIN paths up ON up.id = ek.path_id
LEFT JOIN ipsw_devices id ON id.ipsw_id = ek.ipsw_id
LEFT JOIN devices d ON d.name = id.device_name
GROUP BY ek.id, i.version, i.buildid, up.path, uk.key, uv.value_type, uv.value, i.created_at;

-- Create indexes on materialized view if they don't exist
DO $$
BEGIN
    -- Indexes on materialized view for lightning-fast searches
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_search_key') THEN
        CREATE INDEX idx_search_key ON entitlements_search USING gin(key gin_trgm_ops);
    END IF;
    
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_search_path') THEN
        CREATE INDEX idx_search_path ON entitlements_search USING gin(file_path gin_trgm_ops);
    END IF;
    
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_search_version') THEN
        CREATE INDEX idx_search_version ON entitlements_search(ios_version);
    END IF;
    
    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_search_id') THEN
        CREATE INDEX idx_search_id ON entitlements_search(id); -- For pagination
    END IF;
END $$;

-- Function to refresh materialized view (call periodically, not per-query)
CREATE OR REPLACE FUNCTION refresh_search_view()
RETURNS void AS $$
BEGIN
    REFRESH MATERIALIZED VIEW CONCURRENTLY entitlements_search;
END;
$$ LANGUAGE plpgsql;

-- Keep administrative helpers off the public Data API/RPC surface.
REVOKE EXECUTE ON FUNCTION refresh_search_view() FROM PUBLIC, anon, authenticated;

-- Enable Row Level Security for all tables
ALTER TABLE ipsws ENABLE ROW LEVEL SECURITY;
ALTER TABLE devices ENABLE ROW LEVEL SECURITY;
ALTER TABLE ipsw_devices ENABLE ROW LEVEL SECURITY;
ALTER TABLE entitlement_keys ENABLE ROW LEVEL SECURITY;
ALTER TABLE entitlement_values ENABLE ROW LEVEL SECURITY;
ALTER TABLE paths ENABLE ROW LEVEL SECURITY;
ALTER TABLE entitlements ENABLE ROW LEVEL SECURITY;

-- Browser roles are read-only. Writes are reserved for direct DB credentials used by automation.
REVOKE ALL ON TABLE ipsws, devices, ipsw_devices, entitlement_keys, entitlement_values, paths, entitlements, entitlements_search FROM anon, authenticated;
REVOKE ALL ON SEQUENCE entitlement_keys_id_seq, entitlement_values_id_seq, paths_id_seq, entitlements_id_seq FROM anon, authenticated;
GRANT SELECT ON TABLE ipsws, devices, ipsw_devices, entitlement_keys, entitlement_values, paths, entitlements, entitlements_search TO anon, authenticated;

-- Prevent future public-schema objects from becoming browser-writable by default.
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
    REVOKE SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER ON TABLES FROM anon, authenticated;
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
    REVOKE USAGE, SELECT, UPDATE ON SEQUENCES FROM anon, authenticated;
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
    REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC, anon, authenticated;

-- Drop existing policies if they exist and recreate them
DO $$
BEGIN
    -- Drop existing policies
    DROP POLICY IF EXISTS "Allow public read access" ON ipsws;
    DROP POLICY IF EXISTS "Allow public read access" ON devices;
    DROP POLICY IF EXISTS "Allow public read access" ON ipsw_devices;
    DROP POLICY IF EXISTS "Allow public read access" ON entitlement_keys;
    DROP POLICY IF EXISTS "Allow public read access" ON entitlement_values;
    DROP POLICY IF EXISTS "Allow public read access" ON paths;
    DROP POLICY IF EXISTS "Allow public read access" ON entitlements;
    DROP POLICY IF EXISTS "Allow anonymous read access on ipsws" ON ipsws;
    DROP POLICY IF EXISTS "Allow anonymous read access on devices" ON devices;
    DROP POLICY IF EXISTS "Allow anonymous read access on ipsw_devices" ON ipsw_devices;
    DROP POLICY IF EXISTS "Allow anonymous read access on entitlement_keys" ON entitlement_keys;
    DROP POLICY IF EXISTS "Allow anonymous read access on entitlement_values" ON entitlement_values;
    DROP POLICY IF EXISTS "Allow anonymous read access on paths" ON paths;
    DROP POLICY IF EXISTS "Allow anonymous read access on entitlements" ON entitlements;
END $$;

-- Create policies for public read-only access (safe for community service)
CREATE POLICY "Allow public read access" ON ipsws FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "Allow public read access" ON devices FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "Allow public read access" ON ipsw_devices FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "Allow public read access" ON entitlement_keys FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "Allow public read access" ON entitlement_values FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "Allow public read access" ON paths FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "Allow public read access" ON entitlements FOR SELECT TO anon, authenticated USING (true);

-- Analyze all tables for optimal query planning
ANALYZE ipsws;
ANALYZE devices;
ANALYZE ipsw_devices;
ANALYZE entitlement_keys;
ANALYZE entitlement_values;
ANALYZE paths;
ANALYZE entitlements;

-- Display table information
SELECT 
    table_name,
    pg_size_pretty(pg_total_relation_size(table_schema||'.'||table_name)) as size
FROM information_schema.tables
WHERE table_schema = 'public' 
    AND table_name IN ('ipsws', 'devices', 'ipsw_devices', 'entitlement_keys', 'entitlement_values', 'paths', 'entitlements')
ORDER BY table_name;
