import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
import { UserRole } from '@prisma/client';
import { AuditService } from '../audit/audit.service';
import { resolveEstateScope } from '../common/estate-scope';
import { RequestUser } from '../common/request-user';
import { PrismaService } from '../database/prisma.service';
import { UpdatePlanFeatureSettingsDto, UpdateRoleFeatureSettingsDto } from './dto/feature-access.dto';
import { FEATURE_CATALOG, FEATURE_KEYS, FeatureKey, PLAN_SCOPED_FEATURE_KEYS } from './feature-catalog';

@Injectable()
export class FeatureAccessService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService
  ) {}

  listCatalog() {
    return FEATURE_CATALOG;
  }

  async getMyFeatures(user: RequestUser) {
    return this.getRoleSettings(user.role, user.estateId);
  }

  async getRoleSettings(role: UserRole, estateId?: string | null) {
    this.assertRole(role);
    const normalizedEstateId = estateId ?? null;
    const features = FEATURE_KEYS;
    const [globalSettings, estateSettings, planFeatures] = await Promise.all([
      this.prisma.globalRoleFeatureSetting.findMany({
        where: { role, feature: { in: features } }
      }),
      normalizedEstateId
        ? this.prisma.estateRoleFeatureSetting.findMany({
            where: { estateId: normalizedEstateId, role, feature: { in: features } }
          })
        : Promise.resolve([]),
      normalizedEstateId ? this.getEstatePlanFeatureSet(normalizedEstateId) : Promise.resolve(null)
    ]);

    const globalByFeature = new Map(globalSettings.map((item) => [item.feature, item]));
    const estateByFeature = new Map(estateSettings.map((item) => [item.feature, item]));
    const planGatedRole = this.isPlanGatedRole(role);

    return FEATURE_CATALOG.map((definition) => {
      const defaultEnabled = definition.defaultRoles.includes(role);
      const global = globalByFeature.get(definition.key);
      const estate = estateByFeature.get(definition.key);
      const globalEnabled = global?.enabled ?? defaultEnabled;
      const roleEnabled = estate?.enabled ?? globalEnabled;
      const planEnabled =
        !planGatedRole || !definition.estateConfigurable || !planFeatures ? true : planFeatures.has(definition.key);
      const effectiveEnabled = roleEnabled && planEnabled;

      return {
        ...definition,
        role,
        estateId: normalizedEstateId,
        defaultEnabled,
        globalEnabled,
        estateEnabled: estate?.enabled ?? null,
        planEnabled,
        effectiveEnabled,
        source: !planEnabled ? 'PLAN' : estate ? 'ESTATE' : global ? 'GLOBAL' : 'DEFAULT'
      };
    });
  }

  async getManageableRoleSettings(role: UserRole, requestedEstateId: string | undefined, actor: RequestUser) {
    const estateId = this.resolveManagementScope(actor, role, requestedEstateId);
    return this.getRoleSettings(role, estateId);
  }

  async isFeatureEnabled(user: RequestUser, feature: FeatureKey | string) {
    if (user.role === UserRole.SUPER_ADMIN) {
      return true;
    }

    const normalizedFeature = this.assertFeature(feature);
    const settings = await this.getRoleSettings(user.role, user.estateId);
    return Boolean(settings.find((item) => item.key === normalizedFeature)?.effectiveEnabled);
  }

  async updateRoleSettings(role: UserRole, dto: UpdateRoleFeatureSettingsDto, actor: RequestUser) {
    this.assertRole(role);
    const estateId = this.resolveManagementScope(actor, role, dto.estateId);
    const settings = dto.settings.map((item) => ({
      feature: this.assertFeature(item.feature),
      enabled: item.enabled
    }));
    await Promise.all(settings.map((item) => this.assertCanSetFeature(actor, role, item.feature, item.enabled, estateId)));

    if (estateId) {
      await this.upsertEstateSettings(estateId, role, settings, actor.id);
    } else {
      await this.upsertGlobalSettings(role, settings, actor.id);
    }

    await this.audit.record({
      estateId,
      actorType: 'USER',
      actorId: actor.id,
      action: 'ROLE_FEATURE_SETTINGS_UPDATED',
      metadata: {
        role,
        scope: estateId ? 'ESTATE' : 'GLOBAL',
        features: settings.map((item) => item.feature)
      }
    });

    return this.getRoleSettings(role, estateId);
  }

  async getPlanSettings(code: string) {
    const normalizedCode = code.trim().toUpperCase();
    const plan = await this.prisma.subscriptionPlan.findUnique({
      where: { code: normalizedCode },
      include: { features: true }
    });
    if (!plan) {
      throw new BadRequestException(`Unknown plan: ${normalizedCode}`);
    }
    const enabledFeatures = new Set(plan.features.map((item) => item.feature));
    return {
      code: plan.code,
      name: plan.name,
      features: FEATURE_CATALOG.map((definition) => ({
        ...definition,
        planCode: plan.code,
        enabled: enabledFeatures.has(definition.key)
      }))
    };
  }

  async updatePlanSettings(code: string, dto: UpdatePlanFeatureSettingsDto, actor: RequestUser) {
    if (actor.role !== UserRole.SUPER_ADMIN) {
      throw new ForbiddenException('Only super admins can manage plan features');
    }
    const normalizedCode = code.trim().toUpperCase();
    const plan = await this.prisma.subscriptionPlan.findUnique({ where: { code: normalizedCode } });
    if (!plan) {
      throw new BadRequestException(`Unknown plan: ${normalizedCode}`);
    }
    const settings = dto.settings.map((item) => ({
      feature: this.assertFeature(item.feature),
      enabled: item.enabled
    }));

    await this.prisma.$transaction(async (tx) => {
      const enabled = settings.filter((item) => item.enabled);
      if (enabled.length) {
        await tx.subscriptionPlanFeature.createMany({
          data: enabled.map((item) => ({
            planId: plan.id,
            feature: item.feature
          })),
          skipDuplicates: true
        });
      }
      const disabled = settings.filter((item) => !item.enabled).map((item) => item.feature);
      if (disabled.length) {
        await tx.subscriptionPlanFeature.deleteMany({
          where: { planId: plan.id, feature: { in: disabled } }
        });
      }
    });

    await this.audit.record({
      estateId: null,
      actorType: 'USER',
      actorId: actor.id,
      action: 'PLAN_FEATURE_SETTINGS_UPDATED',
      metadata: {
        planCode: normalizedCode,
        features: settings
      }
    });

    return this.getPlanSettings(normalizedCode);
  }

  private async upsertGlobalSettings(
    role: UserRole,
    settings: { feature: FeatureKey; enabled: boolean }[],
    updatedById: string
  ) {
    await this.prisma.$transaction(
      settings.map((item) =>
        this.prisma.globalRoleFeatureSetting.upsert({
          where: { role_feature: { role, feature: item.feature } },
          create: {
            role,
            feature: item.feature,
            enabled: item.enabled,
            updatedById
          },
          update: {
            enabled: item.enabled,
            updatedById
          }
        })
      )
    );
  }

  private async upsertEstateSettings(
    estateId: string,
    role: UserRole,
    settings: { feature: FeatureKey; enabled: boolean }[],
    updatedById: string
  ) {
    await this.prisma.$transaction(
      settings.map((item) =>
        this.prisma.estateRoleFeatureSetting.upsert({
          where: { estateId_role_feature: { estateId, role, feature: item.feature } },
          create: {
            estateId,
            role,
            feature: item.feature,
            enabled: item.enabled,
            updatedById
          },
          update: {
            enabled: item.enabled,
            updatedById
          }
        })
      )
    );
  }

  private resolveManagementScope(actor: RequestUser, role: UserRole, requestedEstateId?: string) {
    if (actor.role === UserRole.SUPER_ADMIN) {
      return requestedEstateId ?? null;
    }

    if (actor.role !== UserRole.ESTATE_ADMIN) {
      throw new ForbiddenException('Only super admins and estate admins can manage feature settings');
    }

    if (role !== UserRole.RESIDENT && role !== UserRole.GUARD) {
      throw new ForbiddenException('Estate admins can only manage resident and guard feature settings');
    }

    const estateId = resolveEstateScope(actor, requestedEstateId);
    if (!estateId) {
      throw new ForbiddenException('Estate-scoped feature settings require an estate');
    }
    return estateId;
  }

  private assertRole(role: UserRole) {
    if (!Object.values(UserRole).includes(role)) {
      throw new BadRequestException(`Unknown role: ${role}`);
    }
  }

  private assertFeature(feature: FeatureKey | string) {
    if (!FEATURE_KEYS.includes(feature as FeatureKey)) {
      throw new BadRequestException(`Unknown feature: ${feature}`);
    }
    return feature as FeatureKey;
  }

  private async assertCanSetFeature(
    actor: RequestUser,
    role: UserRole,
    feature: FeatureKey,
    enabled: boolean,
    estateId: string | null
  ) {
    if (actor.role === UserRole.SUPER_ADMIN) {
      return;
    }

    const definition = FEATURE_CATALOG.find((item) => item.key === feature);
    if (!definition?.estateConfigurable || !definition.defaultRoles.includes(role)) {
      throw new ForbiddenException('Estate admins can only toggle supported resident or guard estate features');
    }
    if (enabled && estateId && !(await this.isFeatureInEstatePlan(estateId, feature))) {
      throw new ForbiddenException('This feature is not available on the estate subscription plan');
    }
  }

  private isPlanGatedRole(role: UserRole) {
    return role !== UserRole.SUPER_ADMIN && role !== UserRole.SALES_ADMIN;
  }

  private async getEstatePlanFeatureSet(estateId: string) {
    const estate = await this.prisma.estate.findUnique({
      where: { id: estateId },
      select: {
        subscriptionPlan: {
          select: {
            features: { select: { feature: true } }
          }
        }
      }
    });
    if (!estate?.subscriptionPlan) {
      return null;
    }
    return new Set(estate.subscriptionPlan.features.map((item) => item.feature as FeatureKey));
  }

  private async isFeatureInEstatePlan(estateId: string, feature: FeatureKey) {
    if (!PLAN_SCOPED_FEATURE_KEYS.includes(feature)) {
      return true;
    }
    const planFeatures = await this.getEstatePlanFeatureSet(estateId);
    return planFeatures ? planFeatures.has(feature) : true;
  }
}
