import {
  ConflictException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { randomBytes } from 'node:crypto';
import { Prisma } from '../../generated/prisma/client';
import { ClientStatus, PrimaryMetricType } from '../../generated/prisma/enums';
import { PrismaService } from '../../common/prisma/prisma.service';
import { ActivityLogService } from '../activity-logs/activity-log.service';
import { ServiceLogger } from '../../common/logging/service-logger';
import { CreateClientDto } from './dto/create-client.dto';
import { ListClientsQueryDto } from './dto/list-clients-query.dto';
import { UpdateClientDto } from './dto/update-client.dto';

export type AdminClientRecord = {
  id: string;
  companyName: string;
  website: string;
  contactName: string;
  email: string;
  phone: string;
  primaryMetricType: PrimaryMetricType;
  status: ClientStatus;
  isDeleted: boolean;
  notes: string | null;
  dashboardSlug: string;
  dashboardVisitCount: number;
  lastDashboardVisitedAt: Date | null;
  lastUpdatedAt: Date;
  createdAt: Date;
  updatedAt: Date;
  assignedUserId: string;
};

export type AdminClientsSummary = {
  total: number;
};

export type AdminClientsListResponse = {
  items: AdminClientRecord[];
  meta: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
};

type ClientPayload = {
  companyName: string;
  website: string;
  contactName: string;
  email: string;
  phone: string;
  primaryMetricType: PrimaryMetricType;
  status: ClientStatus;
  notes: string | null;
  assignedUserId: string;
};

@Injectable()
export class ClientsService {
  private readonly logger = new ServiceLogger(ClientsService.name);
  constructor(
    private readonly prisma: PrismaService,
    private readonly activityLogService: ActivityLogService,
  ) {}

  private get clientClient() {
    return this.prisma.client as any;
  }

  async listClients(): Promise<AdminClientRecord[]>;
  async listClients(
    query: undefined,
    assignedUserId: string,
  ): Promise<AdminClientRecord[]>;
  async listClients(
    query: ListClientsQueryDto,
    assignedUserId?: string,
  ): Promise<AdminClientsListResponse>;
  async listClients(
    query?: ListClientsQueryDto,
    assignedUserId?: string,
  ): Promise<AdminClientRecord[] | AdminClientsListResponse> {
    const hasQuery =
      Boolean(query?.search?.trim()) ||
      query?.status !== undefined ||
      query?.page !== undefined ||
      query?.limit !== undefined;

    const where = this.buildClientWhere(query, assignedUserId);

    if (!hasQuery) {
      return this.clientClient.findMany({
        where,
        orderBy: {
          createdAt: 'desc',
        },
        select: this.clientSelect,
      });
    }

    const page = query?.page ?? 1;
    const limit = query?.limit ?? 10;

    const total = await this.clientClient.count({ where });
    const totalPages = Math.max(1, Math.ceil(total / limit));
    const safePage = Math.min(page, totalPages);

    const items = await this.clientClient.findMany({
      where,
      orderBy: {
        createdAt: 'desc',
      },
      skip: (safePage - 1) * limit,
      take: limit,
      select: this.clientSelect,
    });

    return {
      items,
      meta: {
        page: safePage,
        limit,
        total,
        totalPages,
      },
    };
  }

  async getClientById(
    clientId: string,
    assignedUserId?: string,
  ): Promise<AdminClientRecord> {
    const client = await this.clientClient.findFirst({
      where: {
        id: clientId,
        ...(assignedUserId ? { assignedUserId, isDeleted: false } : {}),
      },
      select: this.clientSelect,
    });

    if (!client) {
      throw new NotFoundException('Client not found.');
    }

    return client;
  }

  async getClientBySlug(dashboardSlug: string): Promise<AdminClientRecord> {
    const client = await this.clientClient.findFirst({
      where: {
        dashboardSlug,
        isDeleted: false,
        status: 'active',
      },
      select: this.clientSelect,
    });

    if (!client) {
      throw new NotFoundException('Client not found.');
    }

    return client;
  }

  async createClient(dto: CreateClientDto, actorUserId?: string | null) {
    const payload = this.normalizePayload(dto);
    const assignedUser = await this.requireActiveAssignee(
      payload.assignedUserId,
    );

    const client = await this.createWithUniqueSlug(payload);

    await this.recordActivity({
      action: 'client_created',
      entityType: 'client',
      entityId: client.id,
      title: 'Client created',
      description: `${client.contactName} was created and assigned to ${assignedUser.name}.`,
      actorUserId,
      clientId: client.id,
      metadata: {
        companyName: client.companyName,
        assignedUserId: client.assignedUserId,
        status: client.status,
      },
    });
    this.logger.log('client.created', {
      clientId: client.id, actorUserId, assignedUserId: client.assignedUserId,
    });

    return client;
  }

  async updateClient(
    clientId: string,
    dto: UpdateClientDto,
    actorUserId?: string | null,
  ) {
    const current = await this.clientClient.findFirst({
      where: {
        id: clientId,
        isDeleted: false,
      },
      select: this.clientSelect,
    });

    if (!current) {
      throw new NotFoundException('Client not found.');
    }

    const payload = this.normalizePayload(dto, current);

    let assignedUserName: string | null = null;
    let currentAssignedUserName: string | null = null;

    if (payload.assignedUserId) {
      assignedUserName = (
        await this.requireActiveAssignee(payload.assignedUserId)
      ).name;
    }

    if (current.assignedUserId !== payload.assignedUserId) {
      currentAssignedUserName = await this.findUserName(current.assignedUserId);
    }

    const updated = await this.clientClient.update({
      where: { id: clientId },
      data: {
        companyName: payload.companyName,
        website: payload.website,
        contactName: payload.contactName,
        email: payload.email,
        phone: payload.phone,
        primaryMetricType: payload.primaryMetricType,
        status: payload.status,
        notes: payload.notes,
        assignedUserId: payload.assignedUserId,
        lastUpdatedAt: new Date(),
      },
      select: this.clientSelect,
    });

    await this.recordActivity({
      action:
        current.assignedUserId !== updated.assignedUserId
          ? 'client_assigned'
          : 'client_updated',
      entityType: 'client',
      entityId: updated.id,
      title:
        current.assignedUserId !== updated.assignedUserId
          ? 'Client reassigned'
          : 'Client updated',
      description: this.describeClientUpdate(
        current,
        updated,
        currentAssignedUserName,
        assignedUserName,
      ),
      actorUserId,
      clientId: updated.id,
      metadata: {
        companyName: updated.companyName,
        assignedUserId: updated.assignedUserId,
        status: updated.status,
      },
    });
    this.logger.log('client.updated', {
      clientId: updated.id, actorUserId, assignedUserId: updated.assignedUserId,
      reassigned: current.assignedUserId !== updated.assignedUserId,
    });

    return updated;
  }

  async softDeleteClient(clientId: string, actorUserId?: string | null) {
    const current = await this.clientClient.findFirst({
      where: {
        id: clientId,
        isDeleted: false,
      },
      select: this.clientSelect,
    });

    if (!current) {
      throw new NotFoundException('Client not found.');
    }

    const deleted = await this.clientClient.update({
      where: { id: clientId },
      data: {
        isDeleted: true,
        status: 'inactive',
        lastUpdatedAt: new Date(),
      },
      select: this.clientSelect,
    });

    await this.recordActivity({
      action: 'client_status_changed',
      entityType: 'client',
      entityId: deleted.id,
      title: 'Client deleted',
      description: `${deleted.contactName} was marked as deleted.`,
      actorUserId,
      clientId: deleted.id,
      metadata: {
        companyName: deleted.companyName,
        status: deleted.status,
      },
    });
    this.logger.log('client.deleted', { clientId: deleted.id, actorUserId });

    return deleted;
  }

  async restoreClient(clientId: string, actorUserId?: string | null) {
    const current = await this.clientClient.findFirst({
      where: {
        id: clientId,
        isDeleted: true,
      },
      select: this.clientSelect,
    });

    if (!current) {
      throw new NotFoundException('Client not found.');
    }

    const restored = await this.clientClient.update({
      where: { id: clientId },
      data: {
        isDeleted: false,
        status: 'active',
        lastUpdatedAt: new Date(),
      },
      select: this.clientSelect,
    });

    await this.recordActivity({
      action: 'client_status_changed',
      entityType: 'client',
      entityId: restored.id,
      title: 'Client restored',
      description: `${restored.companyName} was restored and reactivated.`,
      actorUserId,
      clientId: restored.id,
      metadata: {
        companyName: restored.companyName,
        status: restored.status,
      },
    });
    this.logger.log('client.restored', { clientId: restored.id, actorUserId });

    return restored;
  }

  private async createWithUniqueSlug(payload: ClientPayload) {
    for (let attempt = 0; attempt < 5; attempt += 1) {
      const dashboardSlug = this.buildDashboardSlug();

      try {
        return await this.clientClient.create({
          data: {
            companyName: payload.companyName,
            website: payload.website,
            contactName: payload.contactName,
            email: payload.email,
            phone: payload.phone,
            primaryMetricType: payload.primaryMetricType,
            status: payload.status,
            notes: payload.notes,
            dashboardSlug,
            assignedUserId: payload.assignedUserId,
            isDeleted: false,
            lastUpdatedAt: new Date(),
          },
          select: this.clientSelect,
        });
      } catch (error) {
        if (!this.isUniqueSlugConflict(error) || attempt === 4) {
          throw error;
        }
      }
    }

    throw new ConflictException(
      'Unable to generate a unique client dashboard slug.',
    );
  }

  private async requireActiveAssignee(userId: string) {
    const user = await this.prisma.user.findFirst({
      where: {
        id: userId,
        role: { in: ['super_admin', 'seo_manager'] },
        status: 'active',
        isDeleted: false,
      },
      select: {
        id: true,
        name: true,
      },
    });

    if (!user) {
      throw new NotFoundException('Assigned user not found or inactive.');
    }

    return user;
  }

  private async findUserName(userId: string) {
    const user = await this.prisma.user.findFirst({
      where: { id: userId },
      select: { name: true },
    });

    return user?.name ?? 'Unknown user';
  }

  private describeClientUpdate(
    current: AdminClientRecord,
    updated: AdminClientRecord,
    currentAssignedUserName: string | null,
    assignedUserName: string | null,
  ) {
    const changes = [
      this.describeChange(
        'Company name',
        current.companyName,
        updated.companyName,
      ),
      this.describeChange('Website', current.website, updated.website),
      this.describeChange(
        'Contact name',
        current.contactName,
        updated.contactName,
      ),
      this.describeChange('Email', current.email, updated.email),
      this.describeChange('Phone', current.phone, updated.phone),
      this.describeChange(
        'Primary metric type',
        this.capitalize(current.primaryMetricType),
        this.capitalize(updated.primaryMetricType),
      ),
      this.describeChange(
        'Status',
        this.capitalize(current.status),
        this.capitalize(updated.status),
      ),
      current.notes !== updated.notes ? 'Internal notes were updated' : null,
      current.assignedUserId !== updated.assignedUserId
        ? `Assigned user changed from "${currentAssignedUserName ?? 'Unknown user'}" to "${assignedUserName ?? 'Unknown user'}"`
        : null,
    ].filter((change): change is string => Boolean(change));

    const clientLabel = updated.companyName || updated.contactName;
    return changes.length > 0
      ? `${clientLabel}: ${changes.join('; ')}.`
      : `${clientLabel} was updated with no visible field changes.`;
  }

  private describeChange(label: string, before: string, after: string) {
    return before === after
      ? null
      : `${label} changed from "${before}" to "${after}"`;
  }

  private capitalize(value: string) {
    return value.charAt(0).toUpperCase() + value.slice(1);
  }

  private normalizePayload(
    dto: CreateClientDto | UpdateClientDto,
    current?: AdminClientRecord,
  ) {
    return {
      companyName:
        typeof dto.companyName === 'string'
          ? dto.companyName.trim()
          : (current?.companyName ?? ''),
      website:
        typeof dto.website === 'string'
          ? dto.website.trim()
          : (current?.website ?? ''),
      contactName:
        typeof dto.contactName === 'string'
          ? dto.contactName.trim()
          : (current?.contactName ?? ''),
      email:
        typeof dto.email === 'string'
          ? dto.email.trim().toLowerCase()
          : (current?.email ?? ''),
      phone:
        typeof dto.phone === 'string'
          ? dto.phone.trim()
          : (current?.phone ?? ''),
      primaryMetricType:
        dto.primaryMetricType ?? current?.primaryMetricType ?? 'order',
      status: dto.status ?? current?.status ?? 'active',
      notes:
        dto.notes === undefined
          ? (current?.notes ?? null)
          : dto.notes?.trim() || null,
      assignedUserId: dto.assignedUserId ?? current?.assignedUserId ?? '',
    };
  }

  private buildDashboardSlug() {
    // Dashboard URLs are access links, so use an opaque, cryptographically
    // random value rather than a guessable company-name-based slug.
    return randomBytes(24).toString('base64url');
  }

  private buildClientWhere(
    query?: ListClientsQueryDto,
    assignedUserId?: string,
  ): Prisma.ClientWhereInput {
    const search = query?.search?.trim();
    const status =
      assignedUserId && query?.status === 'deleted' ? undefined : query?.status;

    return {
      ...(assignedUserId ? { assignedUserId, isDeleted: false } : {}),
      ...(status === 'active'
        ? { status: 'active', isDeleted: false }
        : status === 'inactive'
          ? { status: 'inactive', isDeleted: false }
          : status === 'deleted'
            ? { isDeleted: true }
            : {}),
      ...(search
        ? {
            OR: [
              { companyName: { contains: search, mode: 'insensitive' } },
              { website: { contains: search, mode: 'insensitive' } },
              { contactName: { contains: search, mode: 'insensitive' } },
              { email: { contains: search, mode: 'insensitive' } },
              { phone: { contains: search, mode: 'insensitive' } },
              {
                assignedUser: {
                  is: {
                    name: { contains: search, mode: 'insensitive' },
                  },
                },
              },
            ],
          }
        : {}),
    };
  }

  private isUniqueSlugConflict(error: unknown) {
    return (
      typeof error === 'object' &&
      error !== null &&
      'code' in error &&
      (error as { code?: string }).code === 'P2002'
    );
  }

  private async recordActivity(input: {
    action:
      | 'client_created'
      | 'client_updated'
      | 'client_assigned'
      | 'client_status_changed';
    entityType: 'client';
    entityId: string;
    title: string;
    description: string;
    actorUserId?: string | null;
    clientId?: string | null;
    metadata?: Prisma.InputJsonValue | null;
  }) {
    await this.activityLogService.record({
      action: input.action,
      entityType: input.entityType,
      entityId: input.entityId,
      title: input.title,
      description: input.description,
      actorUserId: input.actorUserId,
      clientId: input.clientId,
      metadata: input.metadata ?? null,
    });
  }

  private clientSelect = {
    id: true,
    companyName: true,
    website: true,
    contactName: true,
    email: true,
    phone: true,
    primaryMetricType: true,
    status: true,
    isDeleted: true,
    notes: true,
    dashboardSlug: true,
    dashboardVisitCount: true,
    lastDashboardVisitedAt: true,
    lastUpdatedAt: true,
    createdAt: true,
    updatedAt: true,
    assignedUserId: true,
  } as const;
}
