import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { EstateSignupStatus, EstateStatus, Prisma, UserRole } from '@prisma/client';
import { AuditService } from '../audit/audit.service';
import {
  calculateProfileCompletion,
  GuardProfileRequirement,
  ProfileCompletionOptions,
  ResidentProfileRequirement
} from '../common/profile-completion';
import { RequestUser } from '../common/request-user';
import { PrismaService } from '../database/prisma.service';
import { UpdateMyProfileDto } from './dto/profile.dto';

type PlanWithFeatures = {
  id: string;
  code: string;
  name: string;
  sortOrder: number;
  publicVisible: boolean;
  residentMarketingEnabled: boolean;
  description: string | null;
  monthlyPriceKobo: number | null;
  oldMonthlyPriceKobo: number | null;
  currency: string;
  customPricing: boolean;
  limits: Prisma.JsonValue;
  metadata: Prisma.JsonValue;
  features: { feature: string }[];
};

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

  async getMe(user: RequestUser) {
    const account = await this.prisma.user.findUnique({
      where: { id: user.id },
      include: {
        estate: {
          include: {
            subscriptionPlan: {
              include: { features: { orderBy: { feature: 'asc' } } }
            },
            gates: { where: { isActive: true }, orderBy: { name: 'asc' } }
          }
        },
        resident: true,
        guard: { include: { assignedGate: true } }
      }
    });

    if (!account) {
      throw new NotFoundException('Profile not found');
    }

    const profile = account.role === UserRole.RESIDENT ? account.resident : account.role === UserRole.GUARD ? account.guard : null;
    const completionOptions = this.profileCompletionOptions(account.estate?.settings, account.role);
    const completion = profile
      ? calculateProfileCompletion(account.role, profile, completionOptions)
      : calculateProfileCompletion(account.role, account, completionOptions);
    const currentPlan = account.estate?.subscriptionPlan ? this.serializePlan(account.estate.subscriptionPlan, false) : null;

    return {
      user: this.redactAccount(account),
      profile,
      completion,
      estate: account.estate ? this.serializeEstate(account.estate) : null,
      currentPlan,
      featureAvailability: this.serializeFeatureAvailability(currentPlan),
      planComparison: await this.getResidentPlanComparison(account.estate?.subscriptionPlan ?? null, account.role)
    };
  }

  async updateMe(user: RequestUser, dto: UpdateMyProfileDto) {
    const account = await this.prisma.user.findUnique({
      where: { id: user.id },
      include: { resident: true, guard: true, estate: { select: { settings: true } } }
    });
    if (!account) {
      throw new NotFoundException('Profile not found');
    }

    const fullName = this.fullName(dto.firstName, dto.lastName);
    const imageMetadata = dto.profileImageMetadata
      ? {
          profileImage: {
            processing: {
              compressed: true,
              resized: true,
              thumbnailGenerated: Boolean(dto.profilePictureThumbnailUrl),
              storageOptimized: true,
              ...dto.profileImageMetadata
            }
          }
        }
      : undefined;

    const updated = await this.prisma.$transaction(async (tx) => {
      const userUpdate = await tx.user.update({
        where: { id: user.id },
        data: {
          firstName: this.clean(dto.firstName),
          lastName: this.clean(dto.lastName),
          name: fullName ?? undefined,
          email: this.clean(dto.email),
          address: this.clean(dto.address),
          profilePictureUrl: this.clean(dto.profilePictureUrl),
          profilePictureThumbnailUrl: this.clean(dto.profilePictureThumbnailUrl),
          profileConfirmed: dto.profileConfirmed,
          metadata: imageMetadata ? this.mergeJson(account.metadata, imageMetadata) : undefined
        }
      });

      let profile: unknown = null;
      if (account.role === UserRole.RESIDENT && account.resident) {
        const completionOptions = this.profileCompletionOptions(account.estate?.settings, UserRole.RESIDENT);
        const draft = {
          ...account.resident,
          firstName: dto.firstName ?? account.resident.firstName,
          lastName: dto.lastName ?? account.resident.lastName,
          phone: account.resident.phone,
          blockNumber: dto.blockNumber ?? account.resident.blockNumber,
          flatNumber: dto.flatNumber ?? account.resident.flatNumber,
          unit: dto.flatNumber ?? account.resident.unit,
          address: dto.address ?? account.resident.address,
          profileConfirmed: dto.profileConfirmed ?? account.resident.profileConfirmed
        };
        const completion = calculateProfileCompletion(UserRole.RESIDENT, draft, completionOptions);
        profile = await tx.resident.update({
          where: { userId: user.id },
          data: {
            firstName: this.clean(dto.firstName),
            lastName: this.clean(dto.lastName),
            name: fullName ?? undefined,
            blockNumber: this.clean(dto.blockNumber),
            flatNumber: this.clean(dto.flatNumber),
            unit: this.clean(dto.flatNumber),
            address: this.clean(dto.address),
            email: this.clean(dto.email),
            profilePictureUrl: this.clean(dto.profilePictureUrl),
            profilePictureThumbnailUrl: this.clean(dto.profilePictureThumbnailUrl),
            emergencyContact: this.toJson(dto.emergencyContact),
            householdMembers: this.toJson(dto.householdMembers),
            profileConfirmed: dto.profileConfirmed,
            profileCompletion: completion.completion,
            metadata: imageMetadata ? this.mergeJson(account.resident.metadata, imageMetadata) : undefined
          }
        });
      }

      if (account.role === UserRole.GUARD && account.guard) {
        const completionOptions = this.profileCompletionOptions(account.estate?.settings, UserRole.GUARD);
        const draft = {
          ...account.guard,
          firstName: dto.firstName ?? account.guard.firstName,
          lastName: dto.lastName ?? account.guard.lastName,
          address: dto.address ?? account.guard.address,
          profilePictureUrl: dto.profilePictureUrl ?? account.guard.profilePictureUrl,
          emergencyContact: dto.emergencyContact ?? account.guard.emergencyContact,
          profileConfirmed: dto.profileConfirmed ?? account.guard.profileConfirmed
        };
        const completion = calculateProfileCompletion(UserRole.GUARD, draft, completionOptions);
        profile = await tx.guard.update({
          where: { userId: user.id },
          data: {
            firstName: this.clean(dto.firstName),
            lastName: this.clean(dto.lastName),
            name: fullName ?? undefined,
            address: this.clean(dto.address),
            profilePictureUrl: this.clean(dto.profilePictureUrl),
            profilePictureThumbnailUrl: this.clean(dto.profilePictureThumbnailUrl),
            emergencyContact: this.toJson(dto.emergencyContact),
            profileConfirmed: dto.profileConfirmed,
            profileCompletion: completion.completion,
            metadata: imageMetadata ? this.mergeJson(account.guard.metadata, imageMetadata) : undefined
          }
        });
      }

      const completion =
        account.role === UserRole.RESIDENT && profile
          ? calculateProfileCompletion(
              UserRole.RESIDENT,
              profile as Record<string, unknown>,
              this.profileCompletionOptions(account.estate?.settings, UserRole.RESIDENT)
            )
          : account.role === UserRole.GUARD && profile
            ? calculateProfileCompletion(
                UserRole.GUARD,
                profile as Record<string, unknown>,
                this.profileCompletionOptions(account.estate?.settings, UserRole.GUARD)
              )
            : calculateProfileCompletion(
                account.role,
                userUpdate,
                this.profileCompletionOptions(account.estate?.settings, account.role)
              );

      await tx.user.update({
        where: { id: user.id },
        data: {
          profileCompletion: completion.completion,
          profileConfirmed: dto.profileConfirmed
        }
      });

      return { user: userUpdate, profile, completion };
    });

    await this.audit.record({
      estateId: user.estateId,
      actorType: 'USER',
      actorId: user.id,
      action: 'PROFILE_UPDATED',
      metadata: {
        role: user.role,
        completion: updated.completion.completion,
        imageProcessed: Boolean(dto.profileImageMetadata)
      }
    });

    return updated;
  }

  async getDemoAccess(user: RequestUser) {
    if (user.role !== UserRole.ESTATE_ADMIN) {
      throw new ForbiddenException('Approved estate admin access is required for the demo');
    }

    const account = await this.prisma.user.findUnique({
      where: { id: user.id },
      select: {
        id: true,
        role: true,
        status: true,
        estate: {
          select: {
            id: true,
            name: true,
            status: true,
            signupStatus: true,
            settings: true
          }
        }
      }
    });
    const estate = account?.estate;
    if (!account || !estate) {
      throw new ForbiddenException('Estate approval is required for demo access');
    }

    const demoAccess = this.jsonObject(this.jsonObject(estate.settings).demoAccess);
    const expiresAt = this.dateString(demoAccess.expiresAt) ?? this.futureIsoDate(120);
    const maxSessionMinutes = Number(demoAccess.maxSessionMinutes ?? 120);
    const approved =
      account.status === 'ACTIVE' &&
      estate.signupStatus === EstateSignupStatus.APPROVED &&
      estate.status === EstateStatus.ACTIVE &&
      demoAccess.approved !== false &&
      new Date(expiresAt).getTime() > Date.now();

    return {
      approved,
      roleRequired: UserRole.ESTATE_ADMIN,
      estate: { id: estate.id, name: estate.name },
      expiresAt,
      maxSessionMinutes,
      reason: approved ? null : 'Estate registration, verification, approval, and active demo time are required'
    };
  }

  async assertUsableProfile(user: RequestUser, feature: string) {
    if (user.role !== UserRole.RESIDENT && user.role !== UserRole.GUARD) {
      return;
    }

    const alwaysAllowed = new Set(['PROFILE_VIEW', 'PROFILE_MANAGE', 'PLAN_COMPARISON_VIEW', 'ANNOUNCEMENTS_VIEW']);
    if (alwaysAllowed.has(feature)) {
      return;
    }

    const account = await this.prisma.user.findUnique({
      where: { id: user.id },
      include: { resident: true, guard: true, estate: { select: { settings: true } } }
    });
    if (!account) {
      throw new ForbiddenException('Profile is required before using the app');
    }

    const profile = user.role === UserRole.RESIDENT ? account.resident : account.guard;
    if (!profile) {
      throw new ForbiddenException('Role profile is required before using the app');
    }

    const completion = calculateProfileCompletion(user.role, profile, this.profileCompletionOptions(account.estate?.settings, user.role));
    if (!completion.complete) {
      throw new ForbiddenException({
        message: 'Complete your profile before using this feature',
        profileCompletion: completion.completion,
        missing: completion.missing
      });
    }
  }

  private async getResidentPlanComparison(currentPlan: PlanWithFeatures | null, role: UserRole) {
    if (!currentPlan || role !== UserRole.RESIDENT || !currentPlan.residentMarketingEnabled) {
      return null;
    }

    const higherPlans = await this.prisma.subscriptionPlan.findMany({
      where: {
        isActive: true,
        publicVisible: true,
        sortOrder: { gt: currentPlan.sortOrder }
      },
      orderBy: { sortOrder: 'asc' },
      include: { features: { orderBy: { feature: 'asc' } } }
    });

    if (!higherPlans.length) {
      return null;
    }

    return {
      showPricing: false,
      current: this.serializePlan(currentPlan, false),
      upgrades: higherPlans.map((plan) => this.serializePlan(plan, false))
    };
  }

  private serializePlan(plan: PlanWithFeatures, includePricing: boolean) {
    const { monthlyPriceKobo, oldMonthlyPriceKobo, currency, customPricing, ...rest } = plan;
    return {
      ...rest,
      features: plan.features.map((item) => item.feature),
      ...(includePricing ? { monthlyPriceKobo, oldMonthlyPriceKobo, currency, customPricing } : {})
    };
  }

  private serializeEstate(estate: {
    id: string;
    name: string;
    location: string;
    settings: Prisma.JsonValue;
    gates?: Array<{ id: string; name: string; type: string; isActive: boolean }>;
    subscriptionPlan?: PlanWithFeatures | null;
  }) {
    const plan = estate.subscriptionPlan ? this.serializePlan(estate.subscriptionPlan, false) : null;
    return {
      id: estate.id,
      name: estate.name,
      location: estate.location,
      currentPlan: plan,
      profileRequirements: this.publicProfileRequirements(estate.settings),
      gates: (estate.gates ?? []).map((gate) => ({
        id: gate.id,
        name: gate.name,
        type: gate.type,
        isActive: gate.isActive
      })),
      settings: {
        announcements: this.publicAnnouncementSettings(estate.settings),
        serviceChargePaymentsEnabled: this.planSupportsServiceChargePayments(plan),
        residentRequestsEnabled: true,
        visitorFeaturesEnabled: true
      }
    };
  }

  private serializeFeatureAvailability(plan: ReturnType<ProfileService['serializePlan']> | null) {
    return {
      serviceChargePayments: this.planSupportsServiceChargePayments(plan),
      residentRequests: true,
      announcements: true,
      visitorPasses: true
    };
  }

  private planSupportsServiceChargePayments(plan: ReturnType<ProfileService['serializePlan']> | null) {
    if (!plan) {
      return false;
    }
    return plan.code === 'BUSINESS' || plan.code === 'ENTERPRISE' || Number(plan.sortOrder ?? 0) >= 40;
  }

  private publicProfileRequirements(settings: Prisma.JsonValue) {
    return {
      resident: this.residentRequiredFields(settings),
      guard: this.guardRequiredFields(settings)
    };
  }

  private publicAnnouncementSettings(settings: Prisma.JsonValue) {
    const announcementSettings = this.jsonObject(this.jsonObject(settings).announcements);
    return {
      allowedCategories: this.stringArray(announcementSettings.allowedCategories) ?? [
        'SECURITY',
        'MAINTENANCE',
        'NEWS',
        'EVENTS',
        'NOTICES',
        'EMERGENCY'
      ],
      dailyPostingLimit: Number(announcementSettings.dailyPostingLimit ?? 10),
      approvalRequiredForFlaggedKeywords: announcementSettings.approvalRequiredForFlaggedKeywords !== false
    };
  }

  private profileCompletionOptions(settings: Prisma.JsonValue | undefined, role: UserRole): ProfileCompletionOptions {
    if (role === UserRole.RESIDENT) {
      return { residentRequiredFields: this.residentRequiredFields(settings) };
    }
    if (role === UserRole.GUARD) {
      return { guardRequiredFields: this.guardRequiredFields(settings) };
    }
    return {};
  }

  private residentRequiredFields(settings: Prisma.JsonValue | undefined): ResidentProfileRequirement[] {
    const configured = this.settingsArray(settings, 'resident');
    const fields = configured?.length
      ? this.filterRequirements(configured, [
          'firstName',
          'lastName',
          'phone',
          'blockNumber',
          'flatNumber',
          'address',
          'estateId',
          'profileConfirmed'
        ] as const)
      : (['firstName', 'lastName', 'phone', 'blockNumber', 'flatNumber', 'address', 'estateId', 'profileConfirmed'] as ResidentProfileRequirement[]);
    const residentSettings = this.roleProfileSettings(settings, 'resident');
    return fields.filter((field) => {
      if (field === 'blockNumber' && residentSettings.usesBlocks === false) {
        return false;
      }
      if (field === 'flatNumber' && residentSettings.usesFlats === false) {
        return false;
      }
      return true;
    });
  }

  private guardRequiredFields(settings: Prisma.JsonValue | undefined): GuardProfileRequirement[] {
    const configured = this.settingsArray(settings, 'guard');
    return configured?.length
      ? this.filterRequirements(configured, [
          'firstName',
          'lastName',
          'phone',
          'guardCode',
          'assignedGateId',
          'assignedShifts',
          'profilePictureUrl',
          'address',
          'emergencyContact',
          'estateId',
          'profileConfirmed'
        ] as const)
      : ([
          'firstName',
          'lastName',
          'phone',
          'guardCode',
          'assignedGateId',
          'assignedShifts',
          'profilePictureUrl',
          'address',
          'emergencyContact',
          'estateId',
          'profileConfirmed'
        ] as GuardProfileRequirement[]);
  }

  private settingsArray(settings: Prisma.JsonValue | undefined, role: 'resident' | 'guard') {
    return this.stringArray(this.roleProfileSettings(settings, role).requiredFields);
  }

  private roleProfileSettings(settings: Prisma.JsonValue | undefined, role: 'resident' | 'guard') {
    const profileRequirements = this.jsonObject(this.jsonObject(settings).profileRequirements);
    return this.jsonObject(profileRequirements[role]);
  }

  private filterRequirements<T extends string>(values: string[], allowed: readonly T[]) {
    const allowedSet = new Set<string>(allowed);
    return values.filter((value): value is T => allowedSet.has(value));
  }

  private stringArray(value: unknown) {
    if (!Array.isArray(value)) {
      return null;
    }
    return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0);
  }

  private jsonObject(value: unknown): Record<string, unknown> {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
      return {};
    }
    return value as Record<string, unknown>;
  }

  private dateString(value: unknown) {
    if (typeof value !== 'string') {
      return null;
    }
    const time = new Date(value).getTime();
    return Number.isFinite(time) ? value : null;
  }

  private futureIsoDate(minutes: number) {
    return new Date(Date.now() + minutes * 60 * 1000).toISOString();
  }

  private redactAccount(account: { refreshToken?: string | null; [key: string]: unknown }) {
    const rest = { ...account };
    delete rest.refreshToken;
    delete rest.estate;
    return rest;
  }

  private fullName(firstName?: string, lastName?: string) {
    const value = [this.clean(firstName), this.clean(lastName)].filter(Boolean).join(' ').trim();
    return value || undefined;
  }

  private clean(value?: string) {
    return typeof value === 'string' ? value.trim() || undefined : undefined;
  }

  private mergeJson(existing: Prisma.JsonValue, patch: Record<string, unknown>) {
    const existingObject =
      existing && typeof existing === 'object' && !Array.isArray(existing) ? (existing as Record<string, unknown>) : {};
    return { ...existingObject, ...patch } as Prisma.InputJsonValue;
  }

  private toJson(value: unknown) {
    if (value === undefined) {
      return undefined;
    }
    return JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
  }
}
