import { randomUUID } from 'crypto';
import { HttpStatus, Injectable } from '@nestjs/common';
import { DatabaseService, logger } from '../../config';
import { RolePermissionEntity } from '../../config/database/entities';
import { PermissionUserContext } from '../../common/access-control/permission-context.types';
import { PermissionResolverService } from '../../common/access-control/permission-resolver.service';
import { createGeneralError } from '../../utils';
import {
  createAccessRoleDto,
  accessRoleResponseSchema,
  accessRolesResponseSchema,
  updateAccessRoleDto,
} from './access-control.dto';
import {
  ALL_PERMISSION_KEYS,
  getDefaultPermissionMap,
  PERMISSION_MODULES,
  PermissionValue,
  SYSTEM_ROLE_TEMPLATES,
} from './permission-catalog';

@Injectable()
export class AccessControlService {
  constructor(
    private readonly db: DatabaseService,
    private readonly permissionResolver: PermissionResolverService,
  ) {}

  getPermissionCatalog() {
    return {
      data: PERMISSION_MODULES,
      message: 'Permission catalog retrieved successfully',
      status: HttpStatus.OK,
      details: {
        total_modules: PERMISSION_MODULES.length,
        total_permissions: ALL_PERMISSION_KEYS.length,
      },
    };
  }

  private slugify(name: string): string {
    return name
      .trim()
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '_')
      .replace(/^_+|_+$/g, '')
      .slice(0, 80);
  }

  private async getRolePermissionsMap(roleId: string) {
    const rows = await this.db.role_permissions.findMany({
      where: { role_id: roleId },
    });

    return rows.reduce(
      (acc: Record<string, string>, row: { permission_key: string; permission_value: string }) => {
        acc[row.permission_key] = row.permission_value;
        return acc;
      },
      {},
    );
  }

  private mergePermissionMap(
    slug: string,
    overrides: Record<string, string> = {},
  ): Record<string, PermissionValue> {
    const defaults = getDefaultPermissionMap(slug);
    const merged = { ...defaults };

    for (const [key, value] of Object.entries(overrides)) {
      if (ALL_PERMISSION_KEYS.includes(key)) {
        merged[key] = value as PermissionValue;
      }
    }

    return merged;
  }

  private formatRoleResponse(
    role: any,
    permissionMap: Record<string, string>,
  ) {
    const { data } = accessRoleResponseSchema.safeParse({
      ...role,
      permissions: permissionMap,
    });

    return data ?? {
      ...role,
      permissions: permissionMap,
    };
  }

  private isDeadlockError(error: unknown): boolean {
    const err = error as { message?: string; errno?: number; code?: string };
    const message = String(err?.message ?? error);
    return (
      message.includes('Deadlock') ||
      err?.errno === 1213 ||
      err?.code === 'ER_LOCK_DEADLOCK'
    );
  }

  private buildPermissionRows(
    roleId: string,
    permissionMap: Record<string, PermissionValue>,
  ) {
    return Object.entries(permissionMap)
      .filter(([permission_key]) =>
        ALL_PERMISSION_KEYS.includes(permission_key),
      )
      .map(([permission_key, permission_value]) => ({
        id: randomUUID(),
        role_id: roleId,
        permission_key,
        permission_value,
      }));
  }

  private async persistRolePermissions(
    roleId: string,
    permissionMap: Record<string, PermissionValue>,
  ) {
    const rows = this.buildPermissionRows(roleId, permissionMap);
    const maxAttempts = 3;

    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        await this.db.runInTransaction(async (manager) => {
          await manager.delete(RolePermissionEntity, { role_id: roleId });
          if (rows.length > 0) {
            await manager.insert(RolePermissionEntity, rows);
          }
        });
        return;
      } catch (error) {
        if (!this.isDeadlockError(error) || attempt === maxAttempts) {
          throw error;
        }
        await new Promise((resolve) => setTimeout(resolve, 50 * attempt));
      }
    }
  }

  private async ensureOwnerPermissionKeysExist(owner_id: string) {
    const ownerRole = await this.db.access_roles.findFirst({
      where: { owner_id, slug: 'owner' },
    });

    if (!ownerRole) {
      return;
    }

    const defaults = getDefaultPermissionMap('owner');
    const existing = await this.getRolePermissionsMap(ownerRole.id);
    const missing = Object.entries(defaults).filter(
      ([permission_key]) =>
        ALL_PERMISSION_KEYS.includes(permission_key) &&
        existing[permission_key] === undefined,
    );

    if (missing.length === 0) {
      return;
    }

    await this.db.role_permissions.createMany({
      data: missing.map(([permission_key, permission_value]) => ({
        id: randomUUID(),
        role_id: ownerRole.id,
        permission_key,
        permission_value,
      })),
    });
  }

  async ensureDefaultRoles(owner_id: string) {
    const existing = await this.db.access_roles.findMany({
      where: { owner_id },
      orderBy: { hierarchy: 'desc' },
    });

    if (existing.length > 0) {
      await this.ensureOwnerPermissionKeysExist(owner_id);
      return existing;
    }

    for (const template of SYSTEM_ROLE_TEMPLATES) {
      const role = await this.db.access_roles.create({
        data: {
          owner_id,
          name: template.name,
          slug: template.slug,
          description: template.description,
          is_system: template.is_system,
          hierarchy: template.hierarchy,
        },
      });

      await this.persistRolePermissions(
        role.id,
        getDefaultPermissionMap(template.slug),
      );
    }

    return this.db.access_roles.findMany({
      where: { owner_id },
      orderBy: { hierarchy: 'desc' },
    });
  }

  async getRoles(owner_id: string) {
    try {
      const roles = await this.ensureDefaultRoles(owner_id);
      const formatted = await Promise.all(
        roles.map(async (role: any) => {
          const permissions = await this.getRolePermissionsMap(role.id);
          return this.formatRoleResponse(
            role,
            this.mergePermissionMap(role.slug, permissions),
          );
        }),
      );

      const { data } = accessRolesResponseSchema.safeParse(formatted);

      return {
        data: data ?? formatted,
        message: 'Access roles retrieved successfully',
        status: HttpStatus.OK,
        details: { total_roles: formatted.length },
      };
    } catch (error) {
      logger.error('Failed to fetch access roles', error);
      throw error;
    }
  }

  async getRoleById(roleId: string, owner_id: string) {
    const role = await this.db.access_roles.findFirst({
      where: { id: roleId, owner_id },
    });

    if (!role) {
      throw createGeneralError(
        'Role not found',
        'role_id',
        HttpStatus.NOT_FOUND,
      );
    }

    const permissions = await this.getRolePermissionsMap(role.id);

    return {
      data: this.formatRoleResponse(
        role,
        this.mergePermissionMap(role.slug, permissions),
      ),
      message: 'Access role retrieved successfully',
      status: HttpStatus.OK,
      details: null,
    };
  }

  async createRole(data: createAccessRoleDto, owner_id: string) {
    try {
      const baseSlug = this.slugify(data.name);
      let slug = baseSlug || `role_${Date.now()}`;
      let suffix = 1;

      while (
        await this.db.access_roles.findFirst({
          where: { owner_id, slug },
        })
      ) {
        slug = `${baseSlug}_${suffix}`;
        suffix += 1;
      }

      let permissionMap = getDefaultPermissionMap('member');

      if (data.source_role_id) {
        const sourceRole = await this.db.access_roles.findFirst({
          where: { id: data.source_role_id, owner_id },
        });

        if (!sourceRole) {
          throw createGeneralError(
            'Source role not found',
            'source_role_id',
            HttpStatus.NOT_FOUND,
          );
        }

        const sourcePermissions = await this.getRolePermissionsMap(
          sourceRole.id,
        );
        permissionMap = this.mergePermissionMap(
          sourceRole.slug,
          sourcePermissions,
        );
      }

      const role = await this.db.access_roles.create({
        data: {
          owner_id,
          name: data.name.trim(),
          slug,
          description: data.description ?? 'NT/APL',
          is_system: false,
          hierarchy: 10,
        },
      });

      await this.persistRolePermissions(role.id, permissionMap);

      return {
        data: this.formatRoleResponse(role, permissionMap),
        message: 'Access role created successfully',
        status: HttpStatus.CREATED,
        details: null,
      };
    } catch (error) {
      logger.error('Failed to create access role', error);
      throw error;
    }
  }

  async updateRole(
    roleId: string,
    data: updateAccessRoleDto,
    owner_id: string,
  ) {
    try {
      const role = await this.db.access_roles.findFirst({
        where: { id: roleId, owner_id },
      });

      if (!role) {
        throw createGeneralError(
          'Role not found',
          'role_id',
          HttpStatus.NOT_FOUND,
        );
      }

      if (role.slug === 'owner') {
        throw createGeneralError(
          'Owner role permissions cannot be modified',
          'role_id',
          HttpStatus.FORBIDDEN,
        );
      }

      const updatePayload: Record<string, unknown> = {};

      if (data.description !== undefined) {
        updatePayload.description = data.description ?? 'NT/APL';
      }

      if (data.name !== undefined && !role.is_system) {
        updatePayload.name = data.name.trim();
      }

      if (Object.keys(updatePayload).length > 0) {
        await this.db.access_roles.update({
          where: { id: roleId },
          data: updatePayload,
        });
      }

      if (data.permissions) {
        const currentPermissions = await this.getRolePermissionsMap(role.id);
        const merged = this.mergePermissionMap(role.slug, {
          ...currentPermissions,
          ...data.permissions,
        });
        await this.persistRolePermissions(role.id, merged);
      }

      const updatedRole = await this.db.access_roles.findFirst({
        where: { id: roleId, owner_id },
      });
      const permissions = await this.getRolePermissionsMap(roleId);

      return {
        data: this.formatRoleResponse(
          updatedRole,
          this.mergePermissionMap(updatedRole.slug, permissions),
        ),
        message: 'Access role updated successfully',
        status: HttpStatus.OK,
        details: null,
      };
    } catch (error) {
      logger.error('Failed to update access role', error);
      throw error;
    }
  }

  async deleteRole(roleId: string, owner_id: string) {
    try {
      const role = await this.db.access_roles.findFirst({
        where: { id: roleId, owner_id },
      });

      if (!role) {
        throw createGeneralError(
          'Role not found',
          'role_id',
          HttpStatus.NOT_FOUND,
        );
      }

      if (role.is_system) {
        throw createGeneralError(
          'System roles cannot be deleted',
          'role_id',
          HttpStatus.FORBIDDEN,
        );
      }

      await this.db.access_roles.delete({
        where: { id: roleId },
      });

      return {
        data: null,
        message: 'Access role deleted successfully',
        status: HttpStatus.OK,
        details: null,
      };
    } catch (error) {
      logger.error('Failed to delete access role', error);
      throw error;
    }
  }

  async resolveAccessRoleIdBySlug(owner_id: string, slug: string) {
    await this.ensureDefaultRoles(owner_id);

    const role = await this.db.access_roles.findFirst({
      where: { owner_id, slug },
      select: { id: true },
    });

    return role?.id ?? null;
  }

  async getMyPermissions(ctx: PermissionUserContext) {
    await this.ensureDefaultRoles(ctx.owner_id);

    const permissions = await this.permissionResolver.resolveForUser(ctx);
    const accessRoleId = await this.permissionResolver.resolveAccessRoleId(ctx);
    const accessRole = accessRoleId
      ? await this.db.access_roles.findFirst({
          where: { id: accessRoleId, owner_id: ctx.owner_id },
          select: { slug: true },
        })
      : null;

    return {
      data: {
        permissions,
        access_role_id: accessRoleId,
        role_slug: accessRole?.slug ?? ctx.role,
      },
      message: 'Permissions retrieved successfully',
      status: HttpStatus.OK,
      details: null,
    };
  }
}
