import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';

import { UsersRepository } from '@/modules/admin/users/users.repository';
import { UploadService } from '@/modules/upload/upload.service';

import { StakeholdersRepository } from './stakeholders.repository';

import { AssignUserDto } from './dto/assign-user.dto';
import { CreateStakeholderDto } from './dto/create-stakeholder.dto';
import { CreateStakeholderTypeDto } from './dto/create-stakeholder-type.dto';
import { ListStakeholdersDto } from './dto/list-stakeholders.dto';
import { UpdateStakeholderDto } from './dto/update-stakeholder.dto';
import { UpdateStakeholderTypeDto } from './dto/update-stakeholder-type.dto';
import type { StakeholderDashboardSummary } from './stakeholder-dashboard-summary.types';

const LOGO_STORAGE_FOLDER = 'stakeholders';
// 'svg' is listed separately because the upload module treats SVG as its
// own type (an SVG can contain scripts, so it is never a plain 'image').
// Keeping it here preserves the existing behavior: SVG logos are allowed.
const LOGO_ALLOWED_MEDIA_TYPES = ['image', 'svg'];
const DEFAULT_SORT_BY = 'createdAt' as const;
const DEFAULT_SORT_ORDER = 'desc' as const;

@Injectable()
export class StakeholdersService {
  constructor(
    private readonly stakeholders: StakeholdersRepository,
    private readonly users: UsersRepository,
    private readonly uploads: UploadService,
  ) {}

  /**
   * GET /stakeholders
   *
   * Return stakeholder list.
   */
  async list(query: ListStakeholdersDto = {}) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 10;
    const sortBy = query.sortBy ?? DEFAULT_SORT_BY;
    const sortOrder = query.sortOrder ?? DEFAULT_SORT_ORDER;
    const result = await this.stakeholders.findMany({
      page,
      limit,
      sortBy,
      sortOrder,
      stakeholderTypeId: query.stakeholderTypeId,
      active: query.active,
      id: query.id,
      search: query.search,
      name: query.name,
      coChair: query.coChair,
      description: query.description,
      stakeholderTypeName: query.stakeholderTypeName,
      stakeholderTypeIds: query.stakeholderTypeIds,
      years: query.years,
      hasUsers: query.hasUsers,
    });

    return {
      // Flatten the stakeholder-user join rows into a plain `users` array,
      // matching the shape of the detail endpoint.
      data: result.data.map(({ users, ...rest }) => ({
        ...rest,
        users: users.map((member) => member.user),
      })),
      meta: {
        page,
        limit,
        total: result.total,
        totalPages: Math.ceil(result.total / limit),
      },
    };
  }

  /**
   * GET /stakeholders/types
   *
   * Return stakeholder types for dropdown.
   */
  listTypes() {
    return this.stakeholders.listTypes();
  }

  /**
   * GET /stakeholders/grouped-by-type
   *
   * Return each stakeholder type with its stakeholders.
   */
  listGroupedByType() {
    return this.stakeholders.listGroupedByType();
  }

  /**
   * GET /stakeholders/working-groups
   *
   * Return active Private Sector stakeholders for Working Group dropdowns.
   */
  listWorkingGroups() {
    return this.stakeholders.listWorkingGroups();
  }

  /**
   * GET /stakeholders/summary
   *
   * Return global stakeholder counts for the admin dashboard.
   */
  getDashboardSummary(): Promise<StakeholderDashboardSummary> {
    return this.stakeholders.getDashboardSummary();
  }

  /**
   * GET /stakeholders/:id
   *
   * Return one stakeholder with users.
   */
  async getOne(id: number) {
    const stakeholder = await this.stakeholders.findByIdWithUsers(id);

    if (!stakeholder) {
      throw new NotFoundException('Stakeholder not found');
    }

    const { users, ...rest } = stakeholder;

    return {
      ...rest,
      users: users.map((member) => member.user),
    };
  }

  /**
   * POST /stakeholders
   *
   * Create stakeholder.
   *
   * relatedStakeholderId is passed separately because repository handles
   * the mutual relation pair:
   *
   * A.relatedStakeholderId = B.id
   * B.relatedStakeholderId = A.id
   */
  async create(dto: CreateStakeholderDto, logoFile?: Express.Multer.File) {
    const name = dto.name.trim();

    await this.ensureStakeholderTypeExists(dto.stakeholderTypeId);

    await this.validateRelatedStakeholder(undefined, dto.relatedStakeholderId);

    const logoUrl = await this.uploadLogo(logoFile);

    const stakeholder = await this.stakeholders.create(
      {
        name,
        stakeholderType: {
          connect: {
            id: dto.stakeholderTypeId,
          },
        },
        logo: logoUrl,
        description: dto.description ?? null,
        active: dto.active ?? true,
      },
      dto.relatedStakeholderId ?? undefined,
    );

    return {
      message: 'Stakeholder created successfully.',
      stakeholder,
    };
  }

  /**
   * PATCH /stakeholders/:id
   *
   * Update stakeholder.
   *
   * If logoFile is provided:
   * 1. Upload new logo
   * 2. Update DB
   * 3. Delete old logo only after DB update succeeds
   */
  async update(
    id: number,
    dto: UpdateStakeholderDto,
    logoFile?: Express.Multer.File,
  ) {
    const existing = await this.ensureExists(id);

    if (dto.stakeholderTypeId !== undefined) {
      await this.ensureStakeholderTypeExists(dto.stakeholderTypeId);
    }

    await this.validateRelatedStakeholder(id, dto.relatedStakeholderId);

    const newLogoUrl = await this.uploadLogo(logoFile);

    const stakeholder = await this.stakeholders.update(
      id,
      {
        ...(dto.name !== undefined && {
          name: dto.name.trim(),
        }),

        ...(dto.stakeholderTypeId !== undefined && {
          stakeholderType: {
            connect: {
              id: dto.stakeholderTypeId,
            },
          },
        }),

        ...(newLogoUrl !== null && {
          logo: newLogoUrl,
        }),

        ...(dto.description !== undefined && {
          description: dto.description,
        }),

        ...(dto.active !== undefined && {
          active: dto.active,
        }),
      },
      dto.relatedStakeholderId,
    );

    if (newLogoUrl && existing.logo && existing.logo !== newLogoUrl) {
      await this.uploads.remove(existing.logo);
    }

    return {
      message: 'Stakeholder updated successfully.',
      stakeholder,
    };
  }

  /**
   * DELETE /stakeholders/:id
   *
   * Soft delete stakeholder.
   */
  async remove(id: number) {
    await this.ensureExists(id);

    await this.stakeholders.softDelete(id);

    return {
      message: 'Stakeholder deleted successfully.',
    };
  }

  /**
   * GET /stakeholders/:id/members
   *
   * List users under a stakeholder.
   */
  async listMembers(id: number) {
    await this.ensureExists(id);

    const members = await this.stakeholders.listMembers(id);

    return members.map((member) => ({
      ...member.user,
      joinedAt: member.createdAt,
    }));
  }

  /**
   * POST /stakeholders/:id/users
   *
   * Assign user to stakeholder.
   */
  async addUser(stakeholderId: number, dto: AssignUserDto) {
    await this.ensureExists(stakeholderId);

    const user = await this.users.findById(dto.userId);

    if (!user) {
      throw new NotFoundException('User not found');
    }

    const existingMembership = await this.stakeholders.findMembership(
      stakeholderId,
      dto.userId,
    );

    if (existingMembership) {
      throw new ConflictException(
        'User is already a member of this stakeholder',
      );
    }

    const membership = await this.stakeholders.addUser(
      stakeholderId,
      dto.userId,
    );

    return {
      message: 'User added to stakeholder.',
      membership,
    };
  }

  /**
   * DELETE /stakeholders/:stakeholderId/users/:userId
   *
   * Remove user from stakeholder.
   */
  async removeUser(stakeholderId: number, userId: number) {
    await this.ensureExists(stakeholderId);

    const membership = await this.stakeholders.findMembership(
      stakeholderId,
      userId,
    );

    if (!membership) {
      throw new NotFoundException('User is not a member of this stakeholder');
    }

    await this.stakeholders.removeUser(stakeholderId, userId);

    return {
      message: 'User removed from stakeholder.',
    };
  }

  // ---------------------------------------------------------------------------
  // Stakeholder type CRUD
  // ---------------------------------------------------------------------------

  /**
   * POST /stakeholders/types
   *
   * Create stakeholder type.
   */
  async createType(userId: number, dto: CreateStakeholderTypeDto) {
    const name = dto.name.trim();

    await this.ensureTypeNameIsUnique(name);

    const type = await this.stakeholders.createType(name, userId);

    return {
      message: 'Stakeholder type created successfully.',
      type,
    };
  }

  /**
   * GET /stakeholders/types/:id
   *
   * Get stakeholder type detail.
   */
  async getType(id: number) {
    const type = await this.stakeholders.findTypeDetailById(id);

    if (!type) {
      throw new NotFoundException('Stakeholder type not found');
    }

    return type;
  }

  /**
   * PATCH /stakeholders/types/:id
   *
   * Update stakeholder type.
   */
  async updateType(id: number, dto: UpdateStakeholderTypeDto) {
    await this.ensureStakeholderTypeExists(id);

    const name = dto.name?.trim();

    if (name !== undefined) {
      await this.ensureTypeNameIsUnique(name, id);
    }

    const type = await this.stakeholders.updateType(id, {
      name,
    });

    return {
      message: 'Stakeholder type updated successfully.',
      type,
    };
  }

  /**
   * DELETE /stakeholders/types/:id
   *
   * Soft delete stakeholder type.
   */
  async removeType(id: number) {
    await this.ensureStakeholderTypeExists(id);

    await this.stakeholders.softDeleteType(id);

    return {
      message: 'Stakeholder type deleted successfully.',
    };
  }

  /**
   * Check duplicate stakeholder type name.
   *
   * excludeId is used when updating so the same row does not conflict
   * with itself.
   */
  private async ensureTypeNameIsUnique(name: string, excludeId?: number) {
    const existing = await this.stakeholders.findTypeByName(name, excludeId);

    if (existing) {
      throw new ConflictException(
        'Stakeholder type with this name already exists',
      );
    }
  }

  /**
   * Upload logo and return uploaded URL.
   *
   * Returns null when no logo file was provided.
   */
  private async uploadLogo(
    logoFile?: Express.Multer.File,
  ): Promise<string | null> {
    if (!logoFile) {
      return null;
    }

    const uploaded = await this.uploads.save(logoFile, LOGO_STORAGE_FOLDER, {
      allowedMediaTypes: LOGO_ALLOWED_MEDIA_TYPES,
    });

    return uploaded.url;
  }

  /**
   * Check stakeholder exists.
   */
  private async ensureExists(id: number) {
    const stakeholder = await this.stakeholders.findById(id);

    if (!stakeholder) {
      throw new NotFoundException('Stakeholder not found');
    }

    return stakeholder;
  }

  /**
   * Check stakeholder type exists.
   */
  private async ensureStakeholderTypeExists(id: number) {
    const stakeholderType = await this.stakeholders.findTypeById(id);

    if (!stakeholderType) {
      throw new NotFoundException('Stakeholder type not found');
    }

    return stakeholderType;
  }

  /**
   * Validate related stakeholder.
   *
   * Rules:
   * - undefined means do not change relation
   * - null means remove relation
   * - number means link to another stakeholder
   * - stakeholder cannot relate to itself
   */
  private async validateRelatedStakeholder(
    currentStakeholderId: number | undefined,
    relatedStakeholderId?: number | null,
  ) {
    if (relatedStakeholderId === undefined || relatedStakeholderId === null) {
      return;
    }

    if (currentStakeholderId === relatedStakeholderId) {
      throw new BadRequestException('Stakeholder cannot be related to itself');
    }

    const relatedStakeholder =
      await this.stakeholders.findById(relatedStakeholderId);

    if (!relatedStakeholder) {
      throw new NotFoundException('Related stakeholder not found');
    }
  }
}
