/**
 * @fileoverview A preparation for the update of using organizations to manage tenants in the admin
 * tenant. This script will do the following in the admin tenant:
 *
 * 1. Create organization template roles and scopes.
 * 2. Create organizations for existing tenants.
 * 3. Add membership records and assign organization roles to existing users.
 * 4. Create machine-to-machine Management API role for each tenant.
 * 5. Create the corresponding machine-to-machine app for each tenant, and assign the Management API role to it.
 *
 * The `down` script will revert the changes.
 *
 * NOTE: In order to avoid unnecessary dirty data, it's recommended disabling the registration of
 * new tenants before running this script and deploying the changes.
 */

import { ConsoleLog, generateStandardId } from '@logto/shared';
import { sql } from '@silverhand/slonik';

import { type AlterationScript } from '../lib/types/alteration.js';

const adminTenantId = 'admin';
const consoleLog = new ConsoleLog();

const alteration: AlterationScript = {
  up: async (transaction) => {
    consoleLog.info('=== Sync tenant organizations ===');

    consoleLog.info('Create organization template roles and scopes');
    await transaction.query(sql`
      insert into public.organization_roles (id, tenant_id, name, description)
      values
        ('owner', ${adminTenantId}, 'owner', 'Owner of the tenant, who has all permissions.'),
        ('admin', ${adminTenantId}, 'admin', 'Admin of the tenant, who has all permissions except managing the tenant settings.'),
        ('member', ${adminTenantId}, 'member', 'Member of the tenant, who has limited permissions on reading and writing the tenant data.');
    `);
    await transaction.query(sql`
      insert into public.organization_scopes (id, tenant_id, name, description)
      values
        ('read-data', ${adminTenantId}, 'read:data', 'Read the tenant data.'),
        ('write-data', ${adminTenantId}, 'write:data', 'Write the tenant data, including creating and updating the tenant.'),
        ('delete-data', ${adminTenantId}, 'delete:data', 'Delete data of the tenant.'),
        ('invite-member', ${adminTenantId}, 'invite:member', 'Invite members to the tenant.'),
        ('remove-member', ${adminTenantId}, 'remove:member', 'Remove members from the tenant.'),
        ('update-member-role', ${adminTenantId}, 'update:member:role', 'Update the role of a member in the tenant.'),
        ('manage-tenant', ${adminTenantId}, 'manage:tenant', 'Manage the tenant settings, including name, billing, etc.');
    `);
    await transaction.query(sql`
      insert into public.organization_role_scope_relations (tenant_id, organization_role_id, organization_scope_id)
      values
        (${adminTenantId}, 'owner', 'read-data'),
        (${adminTenantId}, 'owner', 'write-data'),
        (${adminTenantId}, 'owner', 'delete-data'),
        (${adminTenantId}, 'owner', 'invite-member'),
        (${adminTenantId}, 'owner', 'remove-member'),
        (${adminTenantId}, 'owner', 'update-member-role'),
        (${adminTenantId}, 'owner', 'manage-tenant'),
        (${adminTenantId}, 'admin', 'read-data'),
        (${adminTenantId}, 'admin', 'write-data'),
        (${adminTenantId}, 'admin', 'delete-data'),
        (${adminTenantId}, 'admin', 'invite-member'),
        (${adminTenantId}, 'admin', 'remove-member'),
        (${adminTenantId}, 'admin', 'update-member-role'),
        (${adminTenantId}, 'member', 'read-data'),
        (${adminTenantId}, 'member', 'write-data'),
        (${adminTenantId}, 'member', 'invite-member')
    `);

    consoleLog.info('Create organizations for existing tenants');
    const tenants = await transaction.any<{ id: string }>(sql`
      select id
      from public.tenants;
    `);
    await transaction.query(sql`
      insert into public.organizations (id, tenant_id, name)
      values 
        ${sql.join(
          tenants.map(
            (tenant) => sql`(${`t-${tenant.id}`}, ${adminTenantId}, ${`Tenant ${tenant.id}`})`
          ),
          sql`, `
        )};
    `);

    const usersRoles = await transaction.any<{ userId: string; roleName: string }>(sql`
      select
        public.users.id as "userId",
        public.roles.name as "roleName"
      from public.users
      join public.users_roles on public.users_roles.user_id = public.users.id
      join public.roles on public.roles.id = public.users_roles.role_id
      where public.roles.tenant_id = ${adminTenantId}
      and public.roles.name like '%:admin';
    `);

    if (usersRoles.length === 0) {
      consoleLog.warn(
        'No existing admin users found, skip adding membership records for tenant organizations.'
      );
    } else {
      consoleLog.info('Add membership records and assign organization roles to existing users');

      // Add membership records
      await transaction.query(sql`
        insert into public.organization_user_relations (tenant_id, organization_id, user_id)
        values 
          ${sql.join(
            usersRoles.map(
              (userRole) =>
                sql`(${adminTenantId}, ${`t-${userRole.roleName.slice(0, -6)}`}, ${
                  userRole.userId
                })`
            ),
            sql`, `
          )};
      `);
      // We treat all existing users as the owner of the tenant
      await transaction.query(sql`
        insert into public.organization_role_user_relations (tenant_id, organization_id, user_id, organization_role_id)
        values 
          ${sql.join(
            usersRoles.map(
              (userRole) =>
                sql`
                  (
                    ${adminTenantId},
                    ${`t-${userRole.roleName.slice(0, -6)}`},
                    ${userRole.userId},
                    'owner'
                  )
                `
            ),
            sql`, `
          )};
      `);
    }

    consoleLog.info('Create machine-to-machine Management API role for each tenant');
    await transaction.query(sql`
      insert into public.roles (id, tenant_id, name, description, type)
      values
        ${sql.join(
          tenants.map(
            (tenant) =>
              sql`
                (
                  ${`m-${tenant.id}`},
                  ${adminTenantId},
                  ${`machine:mapi:${tenant.id}`},
                  ${`Machine-to-machine role for accessing Management API of tenant '${tenant.id}'.`},
                  'MachineToMachine'
                )
              `
          ),
          sql`, `
        )};
    `);

    const managementApiScopes = await transaction.any<{ id: string; indicator: string }>(sql`
      select public.scopes.id, public.resources.indicator
      from public.resources
      join public.scopes on public.scopes.resource_id = public.resources.id
      where public.resources.indicator like 'https://%.logto.app/api'
      and public.scopes.name = 'all'
      and public.resources.tenant_id = ${adminTenantId};
    `);

    const assertScopeId = (forTenantId: string) => {
      const scope = managementApiScopes.find(
        (scope) => scope.indicator === `https://${forTenantId}.logto.app/api`
      );
      if (!scope) {
        throw new Error(`Cannot find Management API scope for tenant '${forTenantId}'.`);
      }
      return scope.id;
    };

    // Insert role - scope relations
    await transaction.query(sql`
      insert into public.roles_scopes (tenant_id, id, role_id, scope_id)
      values 
        ${sql.join(
          tenants.map(
            (tenant) =>
              sql`
                (
                  ${adminTenantId},
                  ${generateStandardId()},
                  ${`m-${tenant.id}`},
                  ${assertScopeId(tenant.id)}
                )
              `
          ),
          sql`, `
        )};
    `);

    consoleLog.info(
      'Create the corresponding machine-to-machine app for each tenant, and assign the Management API role to it'
    );
    await transaction.query(sql`
      insert into public.applications (id, tenant_id, secret, name, description, type, oidc_client_metadata)
      values 
        ${sql.join(
          tenants.map(
            (tenant) =>
              sql`
                (
                  ${`m-${tenant.id}`},
                  ${adminTenantId},
                  ${generateStandardId(32)},
                  ${`Management API access for ${tenant.id}`},
                  ${`Machine-to-machine app for accessing Management API of tenant '${tenant.id}'.`},
                  'MachineToMachine',
                  ${sql.jsonb({
                    redirectUris: [],
                    postLogoutRedirectUris: [],
                  })}
                )
              `
          ),
          sql`, `
        )};
    `);
    await transaction.query(sql`
      insert into public.applications_roles (tenant_id, id, application_id, role_id)
      values 
        ${sql.join(
          tenants.map(
            (tenant) =>
              sql`
                (
                  ${adminTenantId},
                  ${generateStandardId()},
                  ${`m-${tenant.id}`},
                  ${`m-${tenant.id}`}
                )
              `
          ),
          sql`, `
        )};
    `);

    consoleLog.info('=== Sync tenant organizations done ===');
  },
  down: async (transaction) => {
    consoleLog.info('=== Revert sync tenant organizations ===');

    consoleLog.info('Remove machine-to-machine apps');
    await transaction.query(sql`
      delete from public.applications
      where public.applications.tenant_id = ${adminTenantId}
      and public.applications.id like 'm-%';
    `);

    consoleLog.info('Remove machine-to-machine roles');
    await transaction.query(sql`
      delete from public.roles
      where public.roles.tenant_id = ${adminTenantId}
      and public.roles.id like 'm-%';
    `);

    consoleLog.info('Remove organizations');
    await transaction.query(sql`
      delete from public.organizations
      where public.organizations.tenant_id = ${adminTenantId}
      and public.organizations.id like 't-%';
    `);

    consoleLog.info('Remove organization roles');
    await transaction.query(sql`
      delete from public.organization_roles
      where public.organization_roles.tenant_id = ${adminTenantId}
      and public.organization_roles.id in ('owner', 'admin', 'member');
    `);

    consoleLog.info('Remove organization scopes');
    await transaction.query(sql`
      delete from public.organization_scopes
      where public.organization_scopes.tenant_id = ${adminTenantId}
      and public.organization_scopes.id in (
        'read-data',
        'write-data',
        'delete-data',
        'invite-member',
        'remove-member',
        'update-member-role',
        'manage-tenant'
      );
    `);

    consoleLog.info('=== Revert sync tenant organizations done ===');
  },
};

export default alteration;