\set ON_ERROR_STOP true

-------------------------------------------------------------------------------
-- Create settings database:
-------------------------------------------------------------------------------
COMMENT ON DATABASE cfsettings IS 'Settings used by Mission Portal APIs, no reported data.';

CREATE TABLE IF NOT EXISTS ScheduledReports (
	username		TEXT		NOT NULL,
	query			TEXT		NOT NULL,
	query_id		TEXT		NOT NULL,
	run_classes		TEXT		NOT NULL,
	last_executed 		TEXT,
	email			TEXT,
	email_title		TEXT,
	email_description	TEXT,
	host_include		TEXT[],
	host_exclude		TEXT[],
        already_run		BOOLEAN		DEFAULT FALSE,
        enabled			BOOLEAN		DEFAULT TRUE,
	output			TEXT[]		NOT NULL
    );
COMMENT ON TABLE ScheduledReports IS 'Users scheduled reports.';
COMMENT ON COLUMN ScheduledReports.username IS 'The username of the user who scheduled the report.';
COMMENT ON COLUMN ScheduledReports.query IS 'The SQL query that defines the report.';
COMMENT ON COLUMN ScheduledReports.query_id IS 'The unique identifier of the query.';
COMMENT ON COLUMN ScheduledReports.run_classes IS 'A CFEngine class expression (without ::) such as (January|February|March|April|May|June|July|August|September|October|November|December).GMT_Hr22.Min50_55 describing when the report should be run.';
COMMENT ON COLUMN ScheduledReports.last_executed IS 'The timestamp of when the report was last executed.';
COMMENT ON COLUMN ScheduledReports.email IS 'The email address of the user who scheduled the report.';
COMMENT ON COLUMN ScheduledReports.email_title IS 'The title of the email that contains the report.';
COMMENT ON COLUMN ScheduledReports.email_description IS 'The description which is present in the email providing the report.';
COMMENT ON COLUMN ScheduledReports.host_include IS 'The array of hosts that the report should include.';
COMMENT ON COLUMN ScheduledReports.host_exclude IS 'The array of hosts that the report should exclude (overriding inclusions).';
COMMENT ON COLUMN ScheduledReports.already_run IS 'The boolean flag that indicates whether the report has already run or not.';
COMMENT ON COLUMN ScheduledReports.enabled IS 'The boolean flag that indicates whether the report is enabled or not.';
COMMENT ON COLUMN ScheduledReports.output IS 'The array of output formats (csv, pdf) that the report should generate.';

ALTER TABLE ScheduledReports
ADD COLUMN IF NOT EXISTS excludedhosts json DEFAULT NULL;
COMMENT ON COLUMN ScheduledReports.excludedhosts IS 'The JSON object that stores the hosts that are excluded from the report.';

CREATE TABLE IF NOT EXISTS Settings (
        key                     TEXT		NOT NULL,
        value			JSON
    );
CREATE UNIQUE INDEX IF NOT EXISTS settings_unique_key_idx ON settings (key);
COMMENT ON TABLE Settings IS 'User settings and preferences for RBAC, host not reporting threshold, collision threshold (duplicate host indicator), and Enterprise API log level. Populated when non-default settings are saved.';
COMMENT ON COLUMN Settings.key IS 'The Key of the setting.';
COMMENT ON COLUMN Settings.value IS 'The value of the setting.';

CREATE TABLE IF NOT EXISTS Users (
        username                TEXT		NOT NULL,
        password                TEXT,
        salt                    TEXT,
        name                    TEXT,
        email                   TEXT,
        external                BOOLEAN         DEFAULT FALSE,
        active                  BOOLEAN         DEFAULT FALSE,
        roles                   TEXT[]          DEFAULT '{}',
        time_zone               TEXT            DEFAULT 'Etc/GMT+0',
        ChangeTimestamp         timestamp with time zone       DEFAULT now()
    );
COMMENT ON TABLE Users IS 'User settings (name, email, password, timezone, provenance) and roles associated with the user.';
COMMENT ON COLUMN Users.username IS 'The username of the user.';
COMMENT ON COLUMN Users.password IS 'The hashed password of the user.';
COMMENT ON COLUMN Users.salt IS 'The salt used to hash the password of the user.';
COMMENT ON COLUMN Users.name IS 'The name of the user.';
COMMENT ON COLUMN Users.email IS 'The email address of the user.';
COMMENT ON COLUMN Users.external IS 'The boolean flag that indicates whether the user is an external user or not, defaults to false.';
COMMENT ON COLUMN Users.active IS 'The boolean flag that indicates whether the user is active or not, defaults to false.';
COMMENT ON COLUMN Users.roles IS 'The array of roles that the user has, defaults to an empty array.';
COMMENT ON COLUMN Users.ChangeTimestamp IS 'The timestamp of when the user settings were last changed.';

-- Add time_zone column to users --
-- Default value is Etc/GMT+0 what means no time offset (GMT) by default if time_zone is not specified --
ALTER TABLE Users ADD COLUMN IF NOT EXISTS time_zone TEXT DEFAULT 'Etc/GMT+0';
COMMENT ON COLUMN Users.time_zone IS 'The time zone of the user, defaults to Etc/GMT+0.';

ALTER TABLE Users ADD COLUMN IF NOT EXISTS password_expiration_time timestamp DEFAULT NULL;

CREATE UNIQUE INDEX IF NOT EXISTS users_uniq ON Users (username, external);

ALTER TABLE Users ADD COLUMN IF NOT EXISTS totp_secret varchar(255) DEFAULT NULL;
ALTER TABLE Users ADD COLUMN IF NOT EXISTS two_factor_enabled BOOLEAN DEFAULT FALSE;

COMMENT ON COLUMN Users.totp_secret IS 'The 2FA Time-based one-time password secret token, defaults to NULL.';
COMMENT ON COLUMN Users.two_factor_enabled IS 'The 2FA is enabled flag, defaults to FALSE.';

ALTER TABLE Users ADD COLUMN IF NOT EXISTS first_login_after_2fa_enforced timestamp DEFAULT NULL;
COMMENT ON COLUMN Users.first_login_after_2fa_enforced IS 'Time of the first login after 2FA enforced for all users.';

CREATE TABLE IF NOT EXISTS Roles (
        name                    TEXT		NOT NULL,
        description             TEXT,
        include_rx              TEXT,
        exclude_rx              TEXT,
        ChangeTimestamp         timestamp with time zone       DEFAULT now()
    );
COMMENT ON TABLE Roles IS 'Role definitions that manage host visibility.';
COMMENT ON COLUMN Roles.name IS 'The name of the role, must be unique and not null.';
COMMENT ON COLUMN Roles.description IS 'The description of the role.';
COMMENT ON COLUMN Roles.include_rx IS 'The regular expression that matches classes reported by the host governing what the role can see.';
COMMENT ON COLUMN Roles.exclude_rx IS 'The regular expression that matches classes reported by the host governing what the role cannot see.';
COMMENT ON COLUMN Roles.ChangeTimestamp IS 'The timestamp of when the role was last change.';

ALTER TABLE Roles ADD COLUMN IF NOT EXISTS is_default boolean NULL DEFAULT 'false';
COMMENT ON COLUMN Roles.is_default IS 'The boolean flag that indicates whether the role is the default role for new users, defaults to false.';

-- 3.12.1 drop sketches
ALTER TABLE Roles DROP COLUMN IF EXISTS sketches;

DROP INDEX IF EXISTS roles_uniq CASCADE;
CREATE UNIQUE INDEX IF NOT EXISTS roles_uniq ON Roles (name);

CREATE TABLE IF NOT EXISTS LicenseInfo (
        ExpireTimeStamp         timestamp with time zone    NOT NULL,
        InstallTimestamp        timestamp with time zone,
        Organization            TEXT,
        LicenseType             TEXT,
        LicenseCount            integer
    );
COMMENT ON TABLE LicenseInfo IS 'Information about the currently installed license.';
COMMENT ON COLUMN LicenseInfo.ExpireTimeStamp IS 'The timestamp of when the license expires.';
COMMENT ON COLUMN LicenseInfo.InstallTimestamp IS 'The timestamp of when the license was installed.';
COMMENT ON COLUMN LicenseInfo.Organization IS 'The name of the organization that owns the license.';
COMMENT ON COLUMN LicenseInfo.LicenseType IS 'The type of the license such as Enterprise.';
COMMENT ON COLUMN LicenseInfo.LicenseCount IS 'The number of hosts that the license covers.';

CREATE TABLE IF NOT EXISTS KeysPendingForDeletion (
        HostKey     TEXT
    );
COMMENT ON TABLE KeysPendingForDeletion IS 'Keys of deleted hosts yet to be deleted.';
COMMENT ON COLUMN KeysPendingForDeletion.HostKey IS 'The key of the host that was deleted from the database but not yet from the ppkeys directory.';

 -- FOR OAUTH

CREATE TABLE IF NOT EXISTS  oauth_clients (client_id VARCHAR(80) NOT NULL, client_secret VARCHAR(80) NOT NULL, redirect_uri VARCHAR(2000) NOT NULL, grant_types VARCHAR(80), scope VARCHAR(100), user_id VARCHAR(80), CONSTRAINT client_id_pk PRIMARY KEY (client_id));
COMMENT ON TABLE oauth_clients IS 'OAuth clients';
COMMENT ON COLUMN oauth_clients.client_id IS 'The unique identifier of the OAuth client.';
COMMENT ON COLUMN oauth_clients.client_secret IS 'The secret key of the OAuth client.';
COMMENT ON COLUMN oauth_clients.redirect_uri IS 'The URI that the OAuth client will redirect to after authorization.';
COMMENT ON COLUMN oauth_clients.grant_types IS 'The grant types that the OAuth client supports, such as authorization_code, password, etc.';
COMMENT ON COLUMN oauth_clients.scope IS 'The scope of access that the OAuth client requests, such as read, write, etc.';
COMMENT ON COLUMN oauth_clients.user_id IS 'The user identifier that the OAuth client is associated with.';

CREATE TABLE IF NOT EXISTS  oauth_access_tokens (access_token VARCHAR(40) NOT NULL, client_id VARCHAR(80) NOT NULL, user_id VARCHAR(255), expires TIMESTAMP NOT NULL, scope VARCHAR(2000), CONSTRAINT access_token_pk PRIMARY KEY (access_token));
COMMENT ON TABLE oauth_access_tokens IS 'OAuth access tokens and expiration.';
COMMENT ON COLUMN oauth_access_tokens.access_token IS 'The access token that grants access to the OAuth client.';
COMMENT ON COLUMN oauth_access_tokens.client_id IS 'The client identifier of the OAuth client that obtained the access token.';
COMMENT ON COLUMN oauth_access_tokens.user_id IS 'The user identifier of the user that authorized the access token.';
COMMENT ON COLUMN oauth_access_tokens.expires IS 'The timestamp of when the access token expires.';
COMMENT ON COLUMN oauth_access_tokens.scope IS 'The scope of access that the access token grants.';

ALTER TABLE oauth_access_tokens ADD COLUMN IF NOT EXISTS two_factor_verified_at TIMESTAMP DEFAULT NULL;
COMMENT ON COLUMN oauth_access_tokens.two_factor_verified_at IS 'The timestamp of when the two factor validation was passed for this access token.';

CREATE TABLE IF NOT EXISTS  oauth_authorization_codes (authorization_code VARCHAR(40) NOT NULL, client_id VARCHAR(80) NOT NULL, user_id VARCHAR(255), redirect_uri VARCHAR(2000), expires TIMESTAMP NOT NULL, scope VARCHAR(2000), CONSTRAINT auth_code_pk PRIMARY KEY (authorization_code));
COMMENT ON TABLE oauth_authorization_codes IS 'OAuth authorizations.';
COMMENT ON COLUMN oauth_authorization_codes.authorization_code IS 'The authorization code that grants access to the OAuth client.';
COMMENT ON COLUMN oauth_authorization_codes.client_id IS 'The client identifier of the OAuth client that requested the authorization code.';
COMMENT ON COLUMN oauth_authorization_codes.user_id IS 'The user identifier of the user that authorized the OAuth client.';
COMMENT ON COLUMN oauth_authorization_codes.redirect_uri IS 'The URI that the OAuth client will redirect to after obtaining the authorization code.';
COMMENT ON COLUMN oauth_authorization_codes.expires IS 'The timestamp of when the authorization code expires.';
COMMENT ON COLUMN oauth_authorization_codes.scope IS 'The scope of access that the authorization code grants.';

CREATE TABLE IF NOT EXISTS  oauth_refresh_tokens (refresh_token VARCHAR(40) NOT NULL, client_id VARCHAR(80) NOT NULL, user_id VARCHAR(255), expires TIMESTAMP NOT NULL, scope VARCHAR(2000), CONSTRAINT refresh_token_pk PRIMARY KEY (refresh_token));
COMMENT ON TABLE oauth_refresh_tokens IS 'OAuth token expiration.';
COMMENT ON COLUMN oauth_refresh_tokens.refresh_token IS 'The refresh token that can be used to obtain a new access token.';
COMMENT ON COLUMN oauth_refresh_tokens.client_id IS 'The client identifier of the OAuth client that obtained the refresh token.';
COMMENT ON COLUMN oauth_refresh_tokens.user_id IS 'The user identifier of the user that authorized the OAuth client.';
COMMENT ON COLUMN oauth_refresh_tokens.expires IS 'The timestamp of when the refresh token expires.';
COMMENT ON COLUMN oauth_refresh_tokens.scope IS 'The scope of access that the refresh token grants.';

--CREATE TABLE IF NOT EXISTS  oauth_users (username VARCHAR(255) NOT NULL, password VARCHAR(2000), first_name VARCHAR(255), last_name VARCHAR(255), CONSTRAINT username_pk PRIMARY KEY (username));
CREATE TABLE IF NOT EXISTS  oauth_scopes (scope TEXT, is_default BOOLEAN);
COMMENT ON TABLE oauth_scopes IS 'OAuth scopes.';
COMMENT ON COLUMN oauth_scopes.scope IS 'The name of the OAuth scope, such as read, write, etc.';
COMMENT ON COLUMN oauth_scopes.is_default IS 'The flag that indicates whether the OAuth scope is the default scope for new clients.';

CREATE TABLE IF NOT EXISTS  oauth_jwt (client_id VARCHAR(80) NOT NULL, subject VARCHAR(80), public_key VARCHAR(2000), CONSTRAINT client_id_pk_jwt PRIMARY KEY (client_id));
COMMENT ON TABLE oauth_jwt IS 'OAuth JSON Web Tokens.';
COMMENT ON COLUMN oauth_jwt.client_id IS 'The client identifier of the OAuth client that uses JSON Web Tokens.';
COMMENT ON COLUMN oauth_jwt.subject IS 'The subject of the JSON Web Token, usually the user identifier.';
COMMENT ON COLUMN oauth_jwt.public_key IS 'The public key of the OAuth client that verifies the JSON Web Token signature.';




CREATE OR REPLACE FUNCTION delete_expired_access() RETURNS TRIGGER AS $_$
BEGIN
    DELETE FROM oauth_access_tokens WHERE expires <  now() AT TIME ZONE 'UTC';
    DELETE FROM oauth_refresh_tokens WHERE expires < now() AT TIME ZONE 'UTC';
    RETURN NULL;
END $_$ LANGUAGE 'plpgsql';

DROP TRIGGER IF EXISTS delete_expired_access_token ON oauth_access_tokens;
CREATE TRIGGER delete_expired_access_token AFTER INSERT ON oauth_access_tokens FOR EACH ROW EXECUTE PROCEDURE delete_expired_access();

CREATE TABLE IF NOT EXISTS external_roles_map (
        external_role                    TEXT		NOT NULL,
        internal_role                TEXT,
        ChangeTimestamp         timestamp with time zone       DEFAULT now()
    );
COMMENT ON TABLE external_roles_map IS 'Map of external directory group to Mission Portal RBAC role for automatic association of directory users to Mission Portal roles.';
COMMENT ON COLUMN external_roles_map.external_role IS 'The name of the external directory (LDAP/Active Directory) group.';
COMMENT ON COLUMN external_roles_map.internal_role IS 'The name of the internal Mission Portal role, such as admin, auditor, or guest.';
COMMENT ON COLUMN external_roles_map.ChangeTimestamp IS 'The timestamp of when the mapping was last changed.';

CREATE UNIQUE INDEX IF NOT EXISTS external_roles_uniq ON external_roles_map (external_role);


CREATE TABLE IF NOT EXISTS rbac_permissions (
    "alias" character varying(100) PRIMARY KEY,
    "group" character varying(50),
    "name" character varying(100),
    "description" character varying(200),
    "application" character varying(50),
    "allowed_by_default" boolean DEFAULT false NOT NULL
);
COMMENT ON TABLE rbac_permissions IS 'RBAC permissions.';
COMMENT ON COLUMN rbac_permissions.alias IS 'The unique alias of the RBAC permission, used as the primary key.';
COMMENT ON COLUMN rbac_permissions.group IS 'The group that the RBAC permission belongs to, such as Inventory API, Changes API, Events API, Hosts, etc.';
COMMENT ON COLUMN rbac_permissions.name IS 'The name of the RBAC permission, such as Get inventory report, Get event list, etc.';
COMMENT ON COLUMN rbac_permissions.description IS 'The description of the RBAC permission, explaining what it does and why it is needed.';
COMMENT ON COLUMN rbac_permissions.application IS 'The application that the RBAC permission applies to, such as API, Mission Portal, etc.';
COMMENT ON COLUMN rbac_permissions.allowed_by_default IS 'The flag that indicates whether the RBAC permission is allowed by default for new roles, defaults to false.';

CREATE TABLE IF NOT EXISTS rbac_role_permission (
    "role_id" character varying NOT NULL,
    "permission_alias" character varying NOT NULL,
    CONSTRAINT "rbac_role_permission_role_id_permission_alias" UNIQUE ("role_id", "permission_alias"),
    CONSTRAINT "rbac_role_permission_permission_alias_fkey" FOREIGN KEY (permission_alias) REFERENCES rbac_permissions(alias) ON UPDATE CASCADE ON DELETE CASCADE,
    CONSTRAINT "rbac_role_permission_role_id_fkey" FOREIGN KEY (role_id) REFERENCES roles(name) ON UPDATE CASCADE ON DELETE CASCADE
);
COMMENT ON TABLE rbac_role_permission IS 'This table associates roles to permissions in a 1-to-many relationship.';
COMMENT ON COLUMN rbac_role_permission.role_id IS 'The name of the role that has the permission.';
COMMENT ON COLUMN rbac_role_permission.permission_alias IS 'The alias of the permission that the role has.';

CREATE OR REPLACE FUNCTION set_default_permissions() RETURNS TRIGGER AS $_$
BEGIN
    INSERT INTO rbac_role_permission (permission_alias, role_id) (
    SELECT alias as permission_alias, NEW.name::text as role_id
    FROM rbac_permissions
    WHERE allowed_by_default = true
    );
    RETURN NULL;
END $_$ LANGUAGE 'plpgsql';
COMMENT ON FUNCTION set_default_permissions IS 'This function assigns allowed by default permissions to new roles';

DROP TRIGGER IF EXISTS set_default_permissions_trigger ON roles;
CREATE TRIGGER set_default_permissions_trigger AFTER INSERT ON roles FOR EACH ROW EXECUTE PROCEDURE set_default_permissions();

CREATE OR REPLACE FUNCTION insert_default_permission() RETURNS TRIGGER AS $_$
DECLARE
    role record;
BEGIN
    FOR role IN
        EXECUTE 'SELECT name FROM roles WHERE name NOT IN (''admin'', ''cf_remoteagent'')'
    LOOP
        IF NEW.allowed_by_default  = TRUE THEN
            EXECUTE 'INSERT INTO rbac_role_permission (permission_alias, role_id) ' ||
                    'VALUES ($1, $2) ' ||
                    'ON CONFLICT (role_id, permission_alias) DO NOTHING' USING NEW.alias, role.name;
        END IF;
    END LOOP;
    RETURN NULL;
END $_$ LANGUAGE 'plpgsql';
COMMENT ON FUNCTION insert_default_permission IS 'This function assigns allowed by default permissions to existing roles';

DROP TRIGGER IF EXISTS insert_default_permission_trigger ON rbac_permissions;
CREATE TRIGGER insert_default_permission_trigger AFTER INSERT OR UPDATE ON rbac_permissions FOR EACH ROW EXECUTE PROCEDURE insert_default_permission();


CREATE TABLE IF NOT EXISTS remote_hubs (
    id bigserial  PRIMARY KEY,
    hostkey text,
    ui_name character varying(70) UNIQUE,
    api_url text NULL,
    target_state character varying(20) NOT NULL,
    transport json NOT NULL,
    role character varying(50) NOT NULL
);
COMMENT ON TABLE remote_hubs IS 'Information about federated reporting feeder hubs when federated reporting has been enabled.';
COMMENT ON COLUMN remote_hubs.id IS 'The unique identifier of the remote hub, generated from a sequence.';
COMMENT ON COLUMN remote_hubs.hostkey IS 'The host key of the remote hub.';
COMMENT ON COLUMN remote_hubs.ui_name IS 'The user-friendly name of the remote hub, must be unique among all remote hubs.';
COMMENT ON COLUMN remote_hubs.api_url IS 'The URL of the remote hub API, used for communication and data transfer.';
COMMENT ON COLUMN remote_hubs.target_state IS 'The desired state of the remote hub such as on, paused.';
COMMENT ON COLUMN remote_hubs.transport IS 'The JSON object that stores the transport settings of the remote hub with keys such as mode, ssh_user, ssh_host, ssh_pubkey.';
COMMENT ON COLUMN remote_hubs.role IS 'The role of the remote hub, such as feeder or superhub.';


CREATE TABLE  IF NOT EXISTS federated_reporting_settings (
    "key" character varying DEFAULT 'false' PRIMARY KEY,
    "value" text
);
COMMENT ON TABLE federated_reporting_settings IS 'Federated reporting settings when enabled.';
COMMENT ON COLUMN federated_reporting_settings.key IS 'The name of the federated reporting setting, such as enable_as, enable_request_sent, or target_state.';
COMMENT ON COLUMN federated_reporting_settings.value IS 'The value of the federated reporting setting, such as superhub, 1, or on.';


CREATE TABLE IF NOT EXISTS "inventory_aliases" (
    "inventory_attribute" text NOT NULL,
    "alias" text NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS inventory_aliases_unique ON inventory_aliases ("inventory_attribute", "alias");
COMMENT ON TABLE inventory_aliases IS 'Inventory attributes aliases.';
COMMENT ON COLUMN inventory_aliases.inventory_attribute IS 'The name of the inventory attribute, such as Kernel, Kernel Release, etc.';
COMMENT ON COLUMN inventory_aliases.alias IS 'The alias of the inventory attribute, such as os type, os kernel, etc.';


DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'authentication_types') THEN
        CREATE TYPE authentication_types AS ENUM ('password', 'private_key');
    END IF;
END$$;

CREATE TABLE IF NOT EXISTS ssh_keys (
    "id" bigserial PRIMARY KEY,
    "public_key" text NOT NULL,
    "private_key" text NOT NULL,
    "generated_at" timestamp with time zone DEFAULT now(),
    "generated_by" text  NOT NULL
);
COMMENT ON TABLE ssh_keys IS 'Generated ssh keys.';
COMMENT ON COLUMN ssh_keys.id IS 'The unique identifier of the ssh key, generated from a sequence.';
COMMENT ON COLUMN ssh_keys.public_key IS 'The public key of the ssh key, used for authentication and encryption.';
COMMENT ON COLUMN ssh_keys.private_key IS 'The private key of the ssh key, used for decryption and signing.';
COMMENT ON COLUMN ssh_keys.generated_at IS 'The timestamp of when the ssh key was generated, defaults to the current time.';
COMMENT ON COLUMN ssh_keys.generated_by IS 'The username of the user who generated the ssh key.';

CREATE TABLE IF NOT EXISTS build_projects (
    "id" bigserial  PRIMARY KEY,
    "repository_url" TEXT,
    "branch" TEXT,
    "name" TEXT,
    "authentication_type" authentication_types,
    "username" TEXT,
    "password" TEXT,
    "ssh_private_key" TEXT,
    "ssh_key_id" integer REFERENCES ssh_keys ON DELETE SET NULL DEFAULT NULL,
    "created_at" timestamp with time zone DEFAULT now(),
    "pushed_at" timestamp with time zone DEFAULT NULL,
    "is_local" BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE build_projects IS 'Build application projects.';
COMMENT ON COLUMN build_projects.id IS 'The unique identifier of the build project, generated from a sequence.';
COMMENT ON COLUMN build_projects.repository_url IS 'The URL of the git repository that contains the build project.';
COMMENT ON COLUMN build_projects.branch IS 'The branch of the git repository that the build project uses.';
COMMENT ON COLUMN build_projects.name IS 'The name of the build project, derived from the repository URL and branch.';
COMMENT ON COLUMN build_projects.authentication_type IS 'The type of authentication that the build project uses to access the git repository. Must match authentication_types such as password or private_key.';
COMMENT ON TYPE authentication_types IS 'The available types of authentication that can be used by cfengine build when accessing the project git repository such as password, private_key';
COMMENT ON COLUMN build_projects.username IS 'The username that the build project uses to access the git repository, if applicable.';
COMMENT ON COLUMN build_projects.password IS 'The password that the build project uses to access the git repository, if applicable.';
COMMENT ON COLUMN build_projects.ssh_private_key IS 'This field is not used. Ref ENT-11330.';
COMMENT ON COLUMN build_projects.ssh_key_id IS 'The foreign key that references the ssh_keys table, if applicable.';
COMMENT ON COLUMN build_projects.created_at IS 'The timestamp of when the build project was created.';
COMMENT ON COLUMN build_projects.pushed_at IS 'The timestamp of when the build project was last pushed to the git repository.';
COMMENT ON COLUMN build_projects.is_local IS 'The flag that indicates whether the build project is local or remote.';

DO $$
BEGIN
  IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name='build_projects' and column_name='is_empty')
  THEN
    ALTER TABLE "build_projects" RENAME "is_empty" TO "is_local";
  END IF;
END $$;

ALTER TABLE build_projects ADD COLUMN IF NOT EXISTS is_deployed_locally BOOLEAN DEFAULT FALSE;
COMMENT ON COLUMN build_projects.is_deployed_locally IS 'The flag that indicates whether the build project is deployed locally or not.';
ALTER TABLE build_projects ADD COLUMN IF NOT EXISTS action TEXT DEFAULT NULL;
COMMENT ON COLUMN build_projects.action IS 'The action that the build project performs, such as push, pushAndDeploy, localDeploy.';

ALTER TABLE build_projects ADD COLUMN IF NOT EXISTS classic_policy_set BOOLEAN DEFAULT FALSE;
COMMENT ON COLUMN build_projects.classic_policy_set IS 'The flag that indicates whether the build project is classic policy set (not cfbs) or not.';

CREATE TABLE IF NOT EXISTS cfbs_requests (
    "id" bigserial  PRIMARY KEY,
    "request_name" TEXT NOT NULL,
    "arguments" JSONB,
    "created_at" timestamp with time zone DEFAULT now(),
    "finished_at" timestamp with time zone NULL,
    "response" JSONB
 );
COMMENT ON TABLE cfbs_requests IS 'cfbs requests and responses handled by cf-reactor.';
COMMENT ON COLUMN cfbs_requests.id IS 'The unique identifier of the cfbs request, generated from a sequence.';
COMMENT ON COLUMN cfbs_requests.request_name IS 'The name of the cfbs request, such as init_project, local_deploy, etc.';
COMMENT ON COLUMN cfbs_requests.arguments IS 'The JSONB object that stores the arguments of the cfbs request, such as git, project_id, etc.';
COMMENT ON COLUMN cfbs_requests.created_at IS 'The timestamp of when the cfbs request was created.';
COMMENT ON COLUMN cfbs_requests.finished_at IS 'The timestamp of when the cfbs request was finished, may be null if the request is still in progress.';
COMMENT ON COLUMN cfbs_requests.response IS 'The JSONB object that stores the response of the cfbs request, such as status, details, etc.';

CREATE OR REPLACE FUNCTION process_cfbs_requests() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'UPDATE') THEN
    IF (NEW.finished_at IS NOT NULL) THEN
        PERFORM pg_notify('cfbs_request_done', NEW.id::text);
    END IF;
ELSIF (TG_OP = 'INSERT') THEN
    PERFORM pg_notify('new_cfbs_request', NEW.id::text);
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER cfbs_requests_insert_update_trigger
AFTER INSERT OR UPDATE ON cfbs_requests
FOR EACH ROW EXECUTE FUNCTION process_cfbs_requests();

CREATE TABLE IF NOT EXISTS build_modules (
    "name" TEXT,
    "readme" TEXT NULL,
    "description" TEXT NULL,
    "version" TEXT NOT NULL,
    "author" JSONB DEFAULT NULL,
    "updated" timestamp with time zone NULL,
    "downloads" integer DEFAULT 0,
    "repo" TEXT NULL,
    "documentation" TEXT NULL,
    "website" TEXT NULL,
    "subdirectory" TEXT NULL,
    "commit" TEXT,
    "dependencies" JSONB DEFAULT NULL,
    "tags" JSONB DEFAULT NULL,
    "versions" JSONB DEFAULT NULL,
    "latest" BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE build_modules IS 'Information about build modules available from the index (build.cfengine.com).';
COMMENT ON COLUMN build_modules.name IS 'The name of the build module.';
COMMENT ON COLUMN build_modules.readme IS 'The readme file content of the build module in HTML.';
COMMENT ON COLUMN build_modules.description IS 'The description of the build module.';
COMMENT ON COLUMN build_modules.version IS 'The version of the build module.';
COMMENT ON COLUMN build_modules.author IS 'The author information of the build module as a JSON object with keys such as url, name, image.';
COMMENT ON COLUMN build_modules.updated IS 'The last updated time of the build module.';
COMMENT ON COLUMN build_modules.downloads IS 'The number of downloads of the build module.';
COMMENT ON COLUMN build_modules.repo IS 'The repository URL of the build module.';
COMMENT ON COLUMN build_modules.documentation IS 'The documentation URL of the build module.';
COMMENT ON COLUMN build_modules.website IS 'The website URL of the build module.';
COMMENT ON COLUMN build_modules.subdirectory IS 'The subdirectory of the build module in the repository.';
COMMENT ON COLUMN build_modules.commit IS 'The commit hash of the build module.';
COMMENT ON COLUMN build_modules.dependencies IS 'The dependencies of the build module as a JSON object.';
COMMENT ON COLUMN build_modules.tags IS 'The tags of the build module as a JSON object.';
COMMENT ON COLUMN build_modules.versions IS 'The available versions of the build module as a JSON object.';
COMMENT ON COLUMN build_modules.latest IS 'A flag indicating whether the build module is currently added to the project using the latest version.';

ALTER TABLE build_modules ADD COLUMN IF NOT EXISTS ts_vector tsvector
GENERATED ALWAYS AS
(
  setweight(to_tsvector('simple', build_modules.name), 'A') ||
  setweight(to_tsvector('simple', coalesce(build_modules.description, '')), 'B')
) STORED;

COMMENT ON COLUMN build_modules.ts_vector IS 'Generated ts_vector column based on id and description.';

CREATE INDEX IF NOT EXISTS build_modules_ts_vector_idx ON build_modules USING GIN (ts_vector);

CREATE UNIQUE INDEX IF NOT EXISTS build_modules_unique_idx ON build_modules (name, version);

-- host_groups_seq sequence used in both personal and shared table which guarantee unique id value in both tables
CREATE SEQUENCE IF NOT EXISTS host_groups_seq;
CREATE SEQUENCE IF NOT EXISTS host_groups_priority_seq;

CREATE TABLE IF NOT EXISTS personal_host_groups (
  "id" INTEGER PRIMARY KEY DEFAULT nextval('host_groups_seq'),
  "name" TEXT NOT NULL,
  "description" TEXT NULL,
  "owner" TEXT NOT NULL,
  "creation_time" timestamp with time zone DEFAULT now(),
  "filter" JSONB NOT NULL DEFAULT '{}',
  "filter_sql" TEXT NOT NULL
);
COMMENT ON TABLE personal_host_groups IS 'Personal host groups. Personal host groups can be used for filtering reports that are not shared with others.';
COMMENT ON COLUMN personal_host_groups.id IS 'The primary key of the table, generated from a sequence';
COMMENT ON COLUMN personal_host_groups.name IS 'The name of the personal host group.';
COMMENT ON COLUMN personal_host_groups.description IS 'The optional description of the personal host group.';
COMMENT ON COLUMN personal_host_groups.owner IS 'The username of the owner of the personal host group.';
COMMENT ON COLUMN personal_host_groups.creation_time IS 'The timestamp of when the personal host group was created.';
COMMENT ON COLUMN personal_host_groups.filter IS 'The JSONB object that stores the filter criteria for the personal host group, defaults to an empty object (all hosts are part of a new group by default).';
COMMENT ON COLUMN personal_host_groups.filter_sql IS 'The SQL query that corresponds to the filter criteria for the personal host group.';

ALTER TABLE personal_host_groups
  ADD COLUMN IF NOT EXISTS meta_data JSONB DEFAULT '{}';

CREATE TABLE IF NOT EXISTS shared_host_groups (
  "id" INTEGER PRIMARY KEY DEFAULT nextval('host_groups_seq'),
  "name" TEXT NOT NULL,
  "description" TEXT NULL,
  "priority" INTEGER NOT NULL DEFAULT nextval('host_groups_priority_seq'),
  "creator" TEXT NOT NULL,
  "creation_time" timestamp with time zone DEFAULT now(),
  "deletion_time" timestamp with time zone DEFAULT NULL,
  "filter" JSONB NOT NULL DEFAULT '{}',
  "filter_sql" TEXT NOT NULL
);
COMMENT ON TABLE shared_host_groups IS 'Shared host groups. Shared host groups can be used for filtering as well as distributing CMDB data.';
COMMENT ON COLUMN shared_host_groups.id IS 'The primary key of the table, generated from a sequence.';
COMMENT ON COLUMN shared_host_groups.name IS 'The name of the shared host group, must be unique among currently shared groups.';
COMMENT ON COLUMN shared_host_groups.description IS 'The optional description of the shared host group';
COMMENT ON COLUMN shared_host_groups.priority IS 'The priority of the shared host group, used for sorting and ranking, must be unique among currently shared groups. Note Higher priority numbers override data from lower priority numbers.';
COMMENT ON COLUMN shared_host_groups.creator IS 'The username of the creator of the shared host group.';
COMMENT ON COLUMN shared_host_groups.creation_time IS 'The timestamp of when the shared host group was created.';
COMMENT ON COLUMN shared_host_groups.deletion_time IS 'The timestamp of when the shared host group was deleted, defaults to NULL, indicates that the group is not deleted if NULL.';
COMMENT ON COLUMN shared_host_groups.filter IS 'The JSONB object that stores the filter criteria for the shared host group, defaults to an empty object (all hosts are part of a new group by default).';
COMMENT ON COLUMN shared_host_groups.filter_sql IS 'The SQL query that corresponds to the filter criteria for the shared host group.';

ALTER TABLE shared_host_groups
  DROP CONSTRAINT IF EXISTS shared_host_groups_unique_name,
  DROP CONSTRAINT IF EXISTS shared_host_groups_unique_priority,
  ADD CONSTRAINT shared_host_groups_unique_name UNIQUE NULLS NOT DISTINCT (name, deletion_time),
  ADD CONSTRAINT shared_host_groups_unique_priority UNIQUE NULLS NOT DISTINCT (priority, deletion_time);

ALTER TABLE shared_host_groups
    ADD COLUMN IF NOT EXISTS meta_data JSONB DEFAULT '{}';

-- delete personal host groups of internal (that used for authentication) users
CREATE OR REPLACE FUNCTION clear_user_references()
RETURNS TRIGGER AS $$
BEGIN
  IF OLD.external = FALSE THEN
    DELETE FROM personal_host_groups WHERE owner = OLD.username;
  END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS delete_user ON users;
CREATE TRIGGER delete_user
AFTER DELETE ON users
FOR EACH ROW EXECUTE FUNCTION clear_user_references();

CREATE OR REPLACE PROCEDURE resequence_shared_hosts_groups_priorities() AS $$
DECLARE
    row record;
BEGIN

FOR row IN
        SELECT priority, ROW_NUMBER() OVER (ORDER BY priority) AS adjusted_priority
        FROM shared_host_groups
        WHERE deletion_time IS NULL
    LOOP
        UPDATE shared_host_groups SET priority = row.adjusted_priority
        WHERE shared_host_groups.priority = row.priority AND shared_host_groups.deletion_time IS NULL;
END LOOP;

-- restart host_groups_priority_seq sequence value to the max priority + 1
PERFORM setval('host_groups_priority_seq', COALESCE((SELECT MAX(priority) FROM shared_host_groups), 1));

END $$ LANGUAGE 'plpgsql';

COMMENT ON PROCEDURE  resequence_shared_hosts_groups_priorities
IS 'Removes gaps in priorities and restarts host_groups_priority_seq sequence.';

CREATE
OR REPLACE PROCEDURE increment_priorities_starting_from(priority_value INTEGER) AS $$
DECLARE
group_rec record;
BEGIN
-- Adjustment happens from the last priority (ORDER BY priority DESC) otherwise we will take
-- already occupied priority.

FOR group_rec IN
    SELECT id,  (priority + 1) as adjusted_priority
    FROM shared_host_groups
    WHERE priority >= priority_value
    ORDER BY priority DESC
 LOOP
    UPDATE shared_host_groups
    SET priority = group_rec.adjusted_priority
    WHERE id = group_rec.id;
END LOOP;

-- restart host_groups_priority_seq sequence value to the max priority + 1
PERFORM setval('host_groups_priority_seq', COALESCE((SELECT MAX(priority) FROM shared_host_groups), 1));
END $$ LANGUAGE 'plpgsql';

COMMENT ON PROCEDURE  increment_priorities_starting_from
IS 'Increments priorities starting from the given priority. Useful when you need to insert a group with specific priority.';


CREATE TABLE IF NOT EXISTS favorite_groups (
 "username" text NOT NULL,
 "group_id" integer NOT NULL,
 "created_at" timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE favorite_groups IS 'Users favorite groups.';
COMMENT ON COLUMN favorite_groups.username IS 'The username of the user who added the group to their favorites.';
COMMENT ON COLUMN favorite_groups.group_id IS 'The identifier of the group that the user added to their favorites.';
COMMENT ON COLUMN favorite_groups.created_at IS 'The timestamp of when the user added the group to their favorites.';

ALTER TABLE IF EXISTS favorite_groups
DROP CONSTRAINT IF EXISTS favorite_groups_PK;

ALTER TABLE IF EXISTS favorite_groups  ADD CONSTRAINT favorite_groups_PK UNIQUE ("username", "group_id");

CREATE TABLE IF NOT EXISTS shared_host_groups_data (
  group_id  INT PRIMARY KEY,
  value  jsonb NOT NULL,
  updated_at timestamptz NOT NULL DEFAULT now(),
  epoch bigint NOT NULL DEFAULT 0,
  CONSTRAINT fk_cmdb_group_id FOREIGN KEY (group_id) REFERENCES shared_host_groups(id) ON UPDATE CASCADE ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS shared_host_groups_data_updated_at ON shared_host_groups_data (updated_at);
CREATE INDEX IF NOT EXISTS shared_host_groups_data_epoch ON shared_host_groups_data (epoch);
COMMENT ON TABLE shared_host_groups_data IS 'Shared groups specific CMDB data.';
COMMENT ON COLUMN shared_host_groups_data.value IS 'JSON column with a structure {classes: [json objects], variables: [json objects]}';
COMMENT ON COLUMN shared_host_groups_data.epoch IS 'The sequence number of the latest CMDB change, used for synchronization and conflict resolution.';
COMMENT ON COLUMN shared_host_groups_data.group_id IS 'The identifier of the shared group that the CMDB data belongs to, references the shared_host_groups table.';
COMMENT ON COLUMN shared_host_groups_data.updated_at IS 'The timestamp of when the CMDB data was last updated.';

CREATE OR REPLACE FUNCTION clean_shared_host_groups_data()
RETURNS TRIGGER AS $$
BEGIN
  IF (NEW.deletion_time IS NOT NULL) THEN
    DELETE FROM shared_host_groups_data WHERE group_id = NEW.id;
  END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS clean_shared_host_groups_data_trigger on shared_host_groups;
CREATE TRIGGER clean_shared_host_groups_data_trigger AFTER UPDATE ON shared_host_groups FOR EACH ROW EXECUTE PROCEDURE clean_shared_host_groups_data();

DROP TABLE IF EXISTS failed_login_attempts;

DO
$$
    BEGIN
        IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'failed_attempts_action') THEN
            CREATE TYPE failed_attempts_action AS ENUM ('authentication', 'password_reset');
        END IF;
    END
$$;

ALTER TYPE failed_attempts_action ADD VALUE IF NOT EXISTS 'two_fa_verification';

CREATE TABLE IF NOT EXISTS failed_attempts
(
    ip_address      VARCHAR(50),
    attempts        INT NOT NULL DEFAULT 0,
    action          failed_attempts_action,
    last_attempt    TIMESTAMP    DEFAULT CURRENT_TIMESTAMP,
    timeout_until   TIMESTAMP    DEFAULT NULL,
    timeout_seconds INT          DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS failed_attempts_uniq ON failed_attempts (ip_address, action);
COMMENT ON TABLE failed_attempts IS 'Unsuccessful attempts for different actions like authentication or password reset.';


--------------------------------------------------------
--- Settings that mostly used to be part of policy
--------------------------------------------------------

---------------------------
-- Possible rows:
--  - 'runalerts', '{ "run_interval": int, "jobs": [ { "name": "string", "type": "string", "delay": int, "limit": int } ] }'
--    where all integer values are in seconds and "delay" and "limit" are optional (see Cli_tasks.php for details)
---------------------------
CREATE TABLE IF NOT EXISTS CLITasksSettings (
  task TEXT PRIMARY KEY,
  settings json
);
COMMENT ON TABLE CLITasksSettings IS 'Settings for CLI tasks repeatedly run on the hub (see Cli_tasks.php)';

-- initial settings
INSERT INTO CLITasksSettings VALUES
  ('runalerts',
   '{ "run_interval": 60, "jobs": [ { "name": "all", "type": "all" } ] }')
ON CONFLICT(task) DO NOTHING;

-- procedures to reset settings to defaults (TODO: one procedure taking the task name?)
CREATE OR REPLACE PROCEDURE reset_runalerts_settings_to_default() AS $$
INSERT INTO CLITasksSettings VALUES
  ('runalerts',
   '{ "run_interval": 60, "jobs": [ { "name": "all", "type": "all" } ] }')
ON CONFLICT(task) DO
  UPDATE SET
    settings = '{ "run_interval": 60, "jobs": [ { "name": "all", "type": "all" } ] }'
  WHERE CLITasksSettings.task = 'runalerts';
$$ LANGUAGE SQL;

COMMENT ON PROCEDURE reset_runalerts_settings_to_default IS 'Procedure to reset the settings of the runalerts CLI task to defaults';

CREATE TABLE IF NOT EXISTS system (key TEXT NOT NULL UNIQUE, value JSONB);

COMMENT ON TABLE system
IS 'This table stores key-value pairs for system settings. Each row represents a single setting.';




CREATE OR REPLACE FUNCTION inc_system_value(system_key TEXT)
    RETURNS VOID AS $$
BEGIN
    INSERT INTO system (key, value) VALUES (system_key, to_jsonb(0))
    ON CONFLICT (key)
        DO UPDATE SET value = to_jsonb(system.value::int + 1);
END;
$$
    LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION get_from_system(system_key TEXT, default_value JSONB)
    RETURNS JSONB AS $$
DECLARE current_value JSONB;
BEGIN
    PERFORM key FROM system WHERE key = system_key;
    IF NOT FOUND THEN
        INSERT INTO system (key, value) VALUES (system_key, default_value);
        RETURN default_value;
    END IF;
    SELECT value INTO current_value FROM system WHERE key = system_key;
    RETURN current_value;
END;
$$
    LANGUAGE plpgsql;

COMMENT ON FUNCTION inc_system_value(system_key TEXT)
    IS 'Increment the value in the system table by the specified key, or insert 0 if the key is not found.';

COMMENT ON FUNCTION get_from_system(system_key TEXT, default_value JSONB)
    IS 'Retrieves a JSONB value from the ''system'' table by key. If the key is not not found, the default value is inserted and returned.';

CREATE TABLE IF NOT EXISTS password_reset_tokens
(
    username TEXT NOT NULL,
    token TEXT NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

COMMENT ON TABLE password_reset_tokens IS 'Table stores password reset tokens.';

--- ENT-11976 Audit log -----
DO
$$
    BEGIN
        IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_log_object_type') THEN
            CREATE TYPE audit_log_object_type AS ENUM (
                'User',
                'Role',
                'Settings',
                'Group',
                'Host',
                'Build project',
                'Federated reporting'
                );
        END IF;

        IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_log_action') THEN
            CREATE TYPE audit_log_action AS ENUM (
                'Created',
                'Updated',
                'Deleted',
                'Deployed',
                'Pushed',
                'Module added',
                'Module deleted',
                'Module updated',
                'Module input updated',
                'Data updated',
                'Data created',
                'Data deleted',
                'RBAC updated'
                );
        END IF;
    END
$$;


CREATE TABLE IF NOT EXISTS audit_log
(
    "id" BIGSERIAL PRIMARY KEY,
    "time" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    "actor" TEXT NOT NULL,
    "action" audit_log_action NOT NULL,
    "object_type" audit_log_object_type NOT NULL,
    "object_id" TEXT DEFAULT NULL,
    "object_name" TEXT DEFAULT NULL,
    "details" JSONB default '{}',
    "ip_address" INET default NULL
);

ALTER TABLE audit_log
    ADD COLUMN IF NOT EXISTS object_name TEXT DEFAULT NULL;

CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log("object_type");
CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log("actor");
CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log("time");
CREATE INDEX IF NOT EXISTS idx_audit_log_compound ON audit_log("time", "object_type", "object_id");

COMMENT ON TABLE audit_log IS 'Stores system logs about actions performed by users';
COMMENT ON COLUMN audit_log.id IS 'Unique per event ID';
COMMENT ON COLUMN audit_log.actor IS 'User who performed the action.';
COMMENT ON COLUMN audit_log.object_type IS 'Type of affected object (e.g. user, role, build project).';
COMMENT ON COLUMN audit_log.object_id IS 'Identifier of affected object (e.g. user, role id, build project id), if applicable.';
COMMENT ON COLUMN audit_log.object_name IS 'Name object (e.g. user name, role name, settings name).';
COMMENT ON COLUMN audit_log.action IS 'What was done (e.g. updated, created, deleted, deployed).';
COMMENT ON COLUMN audit_log.details IS 'More details in the free-json format.';
COMMENT ON COLUMN audit_log.time IS 'Time (no timezone) when event happened.';
COMMENT ON COLUMN audit_log.ip_address IS 'IP address of the user who performed the action.';

-- ENT-12185 --
CREATE TABLE IF NOT EXISTS setup_codes
(
    id SERIAL PRIMARY KEY,
    code CHAR(6) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    attempts   INTEGER NOT NULL DEFAULT 0,
    is_revoked BOOLEAN NOT NULL DEFAULT FALSE,
    is_used    BOOLEAN NOT NULL DEFAULT FALSE,
    session_id VARCHAR(64) NULL
);

-- ensures only 1 active code exists
CREATE UNIQUE INDEX IF NOT EXISTS unique_unrevoked_code ON setup_codes ((is_revoked)) WHERE is_revoked = FALSE;

COMMENT ON TABLE setup_codes IS 'Stores setup codes used to complete hub setup and create admin user.';
COMMENT ON COLUMN setup_codes.id IS 'Unique auto-incrementing identifier.';
COMMENT ON COLUMN setup_codes.code IS 'Six-character code.';
COMMENT ON COLUMN setup_codes.created_at IS 'Timestamp indicating when the setup code was created.';
COMMENT ON COLUMN setup_codes.expires_at IS 'Timestamp indicating when the setup code will expire.';
COMMENT ON COLUMN setup_codes.attempts IS 'Number of attempts made to use the setup code.';
COMMENT ON COLUMN setup_codes.is_revoked IS 'Indicates whether the setup code has been revoked before expiration.';
COMMENT ON COLUMN setup_codes.is_used IS 'Indicates whether the setup code has been successfully used.';
COMMENT ON COLUMN setup_codes.session_id IS 'Session identifier linking the setup code to a session.';

CREATE OR REPLACE FUNCTION new_setup_code()
    RETURNS CHAR(6) AS $$
DECLARE
    setup_complete BOOLEAN;
    new_code CHAR(6);
BEGIN
    -- Check if setup is already complete
    SELECT value INTO setup_complete FROM system WHERE "key" = 'is_setup_complete';

    IF setup_complete THEN
        RETURN NULL;
    END IF;

    -- Revoke all existing codes
    UPDATE setup_codes SET is_revoked = TRUE  WHERE is_revoked = FALSE;

    -- Generate a new random 6-digit code
    SELECT LPAD(FLOOR(random() * 1000000)::TEXT, 6, '0') INTO new_code;

    -- Insert the new code with an expiration time of 1 hour from now
    INSERT INTO setup_codes (code, expires_at)
    VALUES (new_code, NOW() + INTERVAL '1 hour');

    RETURN new_code;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION new_setup_code IS 'This function creates setup code and returns NULL when setup complete.';


DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'configuration_type') THEN
  CREATE TYPE configuration_type AS ENUM ('class', 'variable', 'inventory', 'policy_configuration', 'module_input');
  END IF;

  IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cmdb_item_type') THEN
  CREATE TYPE cmdb_item_type AS ENUM ('class', 'variable');
  END IF;
END
$$;

CREATE TABLE IF NOT EXISTS shared_host_groups_data_entries (
    id BIGSERIAL PRIMARY KEY,
    group_id INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    name TEXT,
    description TEXT,
    tags TEXT[],
    type configuration_type NOT NULL,
    meta JSONB DEFAULT '{}',
    CONSTRAINT fk_shared_host_groups_data_entries_group FOREIGN KEY (group_id) REFERENCES shared_host_groups(id) ON UPDATE CASCADE ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS shared_host_groups_data_subentries (
    id BIGSERIAL PRIMARY KEY,
    group_id INT NOT NULL,
    entry_id BIGINT NOT NULL REFERENCES shared_host_groups_data_entries(id) ON DELETE CASCADE,
    item_name VARCHAR(255) NOT NULL,
    item_value JSONB DEFAULT NULL,
    item_type cmdb_item_type NOT NULL,
    CONSTRAINT shared_host_groups_data_subentries_unique UNIQUE (group_id, item_name, item_type)
);

CREATE INDEX IF NOT EXISTS idx_cmdb_items_group_id ON shared_host_groups_data_entries(group_id);

COMMENT ON TABLE shared_host_groups_data_entries IS 'Stores CMDB configurations for hosts.';
COMMENT ON COLUMN shared_host_groups_data_entries.id IS 'Unique identifier for each CMDB entry.';
COMMENT ON COLUMN shared_host_groups_data_entries.group_id IS 'The identifier of the shared group that the CMDB data belongs to, references the shared_host_groups table.';
COMMENT ON COLUMN shared_host_groups_data_entries.created_at IS 'Timestamp when this CMDB entry was created.';
COMMENT ON COLUMN shared_host_groups_data_entries.name IS 'Name of the entry item.';
COMMENT ON COLUMN shared_host_groups_data_entries.description IS 'Description of the entry item.';
COMMENT ON COLUMN shared_host_groups_data_entries.tags IS 'Tags of the entry item.';
COMMENT ON COLUMN shared_host_groups_data_entries.type IS 'Type of the entry item. Allowed values: inventory, variable, class, policy_configuration';
COMMENT ON COLUMN shared_host_groups_data_entries.meta IS 'Meta data of the entry item.';

COMMENT ON TABLE shared_host_groups_data_subentries IS 'Stores individual data elements for each entry item. Implements one-to-many relationship where each CMDB entry can have multiple subentries.';
COMMENT ON COLUMN shared_host_groups_data_subentries.id IS 'Unique identifier for each data element.';
COMMENT ON COLUMN shared_host_groups_data_subentries.group_id IS 'The identifier of the shared group that the CMDB data belongs to, references the shared_host_groups table.';
COMMENT ON COLUMN shared_host_groups_data_subentries.entry_id IS 'Foreign key reference to shared_host_groups_data_entries.id.';
COMMENT ON COLUMN shared_host_groups_data_subentries.item_name IS 'Name of the data. Can be class/variable name';
COMMENT ON COLUMN shared_host_groups_data_subentries.item_value IS 'Value of the subentry data item as JSONB type.';
COMMENT ON COLUMN shared_host_groups_data_subentries.item_type IS 'CMDB data type, can be variable or class.';
 
-- Select 
CREATE OR REPLACE FUNCTION update_rendered_shared_host_groups_data()
RETURNS TRIGGER AS $$
DECLARE
    affected_group_id INT;
    classes_json JSONB := '{}';
    variables_json JSONB := '{}';
    final_json JSONB;
    current_epoch BIGINT;
BEGIN
    -- Identify affected group
    -- For delete rows the group_id is in the OLD and inserted/updated in the NEW
    IF TG_OP = 'DELETE' THEN
        affected_group_id := OLD.group_id;
    ELSE
        affected_group_id := NEW.group_id;
    END IF;
    
    -- Get current epoch and increment it
    SELECT COALESCE(MAX(epoch), 0) + 1 INTO current_epoch FROM shared_host_groups_data;
    
    -- Build classes JSON and store in declared classes_json variable
    -- skip value for classes
    SELECT COALESCE(
        jsonb_object_agg(
            s.item_name,
            jsonb_build_object(
                'tags', CASE 
                        WHEN e.tags IS NOT NULL THEN to_jsonb(e.tags)
                        ELSE '[]'::jsonb
                    END,
                'comment', COALESCE(e.description, '')
            )
        ), '{}'::jsonb
    ) INTO classes_json
    FROM shared_host_groups_data_subentries s
    JOIN shared_host_groups_data_entries e ON s.entry_id = e.id
    WHERE s.group_id = affected_group_id 
    AND s.item_type = 'class';
    
    -- Build variables JSON and store in declared variables_json variable
    SELECT COALESCE(
        jsonb_object_agg(
            s.item_name,
            jsonb_build_object(
                'tags', CASE 
                        WHEN e.tags IS NOT NULL THEN to_jsonb(e.tags)
                        ELSE '[]'::jsonb
                    END,
                'value', COALESCE(s.item_value, 'null'::jsonb),
                'comment', COALESCE(e.description, '')
            )
        ), '{}'::jsonb
    ) INTO variables_json
    FROM shared_host_groups_data_subentries s
    JOIN shared_host_groups_data_entries e ON s.entry_id = e.id
    WHERE s.group_id = affected_group_id 
    AND s.item_type = 'variable';
    
    -- Combine into final JSON structure
    final_json := jsonb_build_object(
        'classes', classes_json,
        'variables', variables_json
    );

    INSERT INTO shared_host_groups_data (group_id, value, updated_at, epoch)
    VALUES (affected_group_id, final_json, NOW(), current_epoch)
    ON CONFLICT (group_id) 
    DO UPDATE SET 
        value = EXCLUDED.value,
        updated_at = EXCLUDED.updated_at,
        epoch = EXCLUDED.epoch;

   RETURN NULL; 
END;
$$ LANGUAGE plpgsql;


DROP TRIGGER IF EXISTS cmdb_update_final_json_trigger ON shared_host_groups_data_subentries;

-- when rows inside shared_host_groups_data_subentries are deleted/updated or inserted we
-- call update_rendered_cmdb_data that will render the final json and update shared_host_groups_data table.
-- cf-reactor on the shared_host_groups_data update will create host-specific JSON file
CREATE TRIGGER cmdb_update_final_json_trigger
    AFTER INSERT OR UPDATE OR DELETE ON shared_host_groups_data_subentries
    FOR EACH ROW
    EXECUTE FUNCTION update_rendered_shared_host_groups_data();


DO $$ BEGIN IF NOT EXISTS (
    SELECT 1
    FROM pg_type
    WHERE typname = 'llm_provider_type'
) THEN CREATE TYPE llm_provider_type AS ENUM (
    'openai',
    'gemini',
    'mistral',
    'ollama',
    'anthropic',
    'openai_like'
);
END IF;
END $$;

CREATE TABLE IF NOT EXISTS llm_configurations (
    id SERIAL PRIMARY KEY,
    provider llm_provider_type NOT NULL,
    model TEXT NOT NULL,
    token TEXT,
    base_url TEXT,
    name VARCHAR(255),
    description TEXT,
    meta JSONB DEFAULT '{}',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
