using Insight.Database;
using System.Data;
using PlanTempus.Core.Database.ConnectionFactory;

namespace PlanTempus.Database.Core.DDL
{
	/// <summary>
	/// This is by purpose not async await
	/// It is intended that this is created with the correct Application User, which is why the schema name is omitted.
	/// </summary>
	public class SetupIdentitySystem : IDbConfigure<SetupIdentitySystem.Command>
    {
        public class Command
        {
            public required string Schema { get; init; }
        }

        Command _command;
        private readonly IDbConnectionFactory _connectionFactory;

        public SetupIdentitySystem(IDbConnectionFactory connectionFactory)
        {
            _connectionFactory = connectionFactory;
        }

        /// <summary>
        /// Creates the system tables in the specified schema within a transaction.
        /// </summary>
        /// <param name="schema">The schema name where the tables will be created.</param>
        public void With(Command command, ConnectionStringParameters parameters = null)
        {
            _command = command;

            using var conn = parameters is null ? _connectionFactory.Create() : _connectionFactory.Create(parameters);
            using var transaction = conn.OpenWithTransaction();
            try
            {
                CreateAccountsTable(conn);
                CreateOrganizationsTable(conn);
                CreateAccountOrganizationsTable(conn);
                SetupRLS(conn);

                transaction.Commit();
            }
            catch (Exception ex)
            {
                transaction.Rollback();
                throw new InvalidOperationException("Failed to SetupIdentitySystem. Transaction is rolled back", ex);
            }
        }


        /// <summary>
        /// Creates the accounts table
        /// </summary>
        void CreateAccountsTable(IDbConnection db)
        {
            var sql = @$"
                CREATE TABLE IF NOT EXISTS {_command.Schema}.accounts (
                    id SERIAL PRIMARY KEY,
                    email VARCHAR(256) NOT NULL UNIQUE,
                    password_hash VARCHAR(256) NOT NULL,
                    security_stamp VARCHAR(36) NOT NULL,
                    email_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
                    access_failed_count INTEGER NOT NULL DEFAULT 0,
                    lockout_enabled BOOLEAN NOT NULL DEFAULT TRUE,
                    lockout_end TIMESTAMPTZ NULL,
                    is_active BOOLEAN NOT NULL DEFAULT TRUE,
                    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    last_login_at TIMESTAMPTZ NULL
                );";

            db.ExecuteSql(sql);

        }

        /// <summary>
        /// Creates the organizations table
        /// </summary>
        void CreateOrganizationsTable(IDbConnection db)
        {
            var sql = @$"
                CREATE TABLE IF NOT EXISTS {_command.Schema}.organizations (
                    id SERIAL PRIMARY KEY,
                    connection_string VARCHAR(500) NOT NULL,
                    is_active BOOLEAN NOT NULL DEFAULT TRUE,
                    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
                );";

            db.ExecuteSql(sql);
        }

        /// <summary>
        /// Creates the account_organizations table
        /// </summary>
        void CreateAccountOrganizationsTable(IDbConnection db)
        {
            var sql = @$"
                CREATE TABLE IF NOT EXISTS {_command.Schema}.account_organizations (
                    account_id INTEGER NOT NULL REFERENCES {_command.Schema}.accounts(id),
                    organization_id INTEGER NOT NULL REFERENCES {_command.Schema}.organizations(id),
                    pin_code VARCHAR(10) NULL,
                    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
                    PRIMARY KEY (account_id, organization_id)
                );";

            db.ExecuteSql(sql);

        }

        /// <summary>
        /// Sets up Row Level Security (RLS) for the organizations and account_organizations tables.
        /// </summary>
        void SetupRLS(IDbConnection db)
        {
            var sql = new[]
                {
                    $"ALTER TABLE {_command.Schema}.organizations ENABLE ROW LEVEL SECURITY;",
                    $"ALTER TABLE {_command.Schema}.account_organizations ENABLE ROW LEVEL SECURITY;",
                    $"DROP POLICY IF EXISTS organization_access ON {_command.Schema}.organizations;",
                    @$"CREATE POLICY organization_access ON {_command.Schema}.organizations
                        USING (id IN (
                            SELECT organization_id
                            FROM {_command.Schema}.account_organizations
                            WHERE account_id = current_setting('app.account_id', TRUE)::INTEGER
                        )) WITH CHECK (true);",
                    $"DROP POLICY IF EXISTS account_organization_access ON {_command.Schema}.account_organizations;",
                    @$"CREATE POLICY account_organization_access ON {_command.Schema}.account_organizations
                        USING (account_id = current_setting('app.account_id', TRUE)::INTEGER) WITH CHECK (true);"
                };

            foreach (var statement in sql)
            {
                db.ExecuteSql(statement);
            }
        }


    }
}