import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { AgentCostRepository, type AssignedTripSource } from './repositories/agent-cost.repository';
import { AssignAgentsDto, CreateAgentCostDto, UpdateAgentCostDto } from './dto';
import { AgentCostEntity } from '../../entities/agent-cost.entity';
import { serviceMessages } from '../../common/constants/messages';

const MSG = serviceMessages('Agent cost');
const IN_USE =
  'Cannot modify this agent cost while trip sources still use it. Reassign those trip sources first.';

export interface AgentCostView {
  id: string;
  name: string;
  percentage: number;
  is_active: boolean;
  created_at: Date;
  updated_at: Date;
  assigned_agents: Array<{ id: string; name: string }>;
}

@Injectable()
export class AgentCostService {
  constructor(private readonly repo: AgentCostRepository) {}

  async findAll(query?: { page?: number; limit?: number; search?: string }) {
    const page = await this.repo.findPaginated({
      page: query?.page,
      limit: query?.limit,
      search: query?.search,
      searchColumns: ['name'],
      order: { name: 'ASC' },
    });
    const assigned = await this.repo.listAssigned(page.items.map((item) => item.id));
    const byCost = groupAssigned(assigned);
    return {
      items: page.items.map((item) => this.toView(item, byCost.get(item.id) ?? [])),
      meta: page.meta,
    };
  }

  async findAllForLookup() {
    const items = await this.repo.find({
      where: { is_active: true },
      select: { id: true, name: true, percentage: true },
      order: { name: 'ASC' },
    });
    return items.map((item) => ({
      id: item.id,
      name: item.name,
      percentage: Number(item.percentage),
    }));
  }

  async findById(id: string): Promise<AgentCostView> {
    const item = await this.repo.findById(id);
    if (!item) throw new NotFoundException(MSG.NOT_FOUND);
    const assigned = await this.repo.listAssigned([id]);
    return this.toView(item, assigned);
  }

  async listUnassignedAgents() {
    return this.repo.listUnassigned();
  }

  async assign(id: string, dto: AssignAgentsDto) {
    const item = await this.findById(id);
    if (!item.is_active) {
      throw new BadRequestException('Activate this agent cost before assigning trip sources');
    }
    const tripSourceIds = [...new Set(dto.trip_source_ids)];
    await this.repo.assignUnassigned(id, tripSourceIds);
    return { id, message: 'Agents assigned successfully' };
  }

  async unassign(id: string, tripSourceId: string) {
    await this.findById(id);
    const cleared = await this.repo.unassign(id, tripSourceId);
    if (!cleared)
      throw new NotFoundException('That trip source is not assigned to this agent cost');
    return { id, message: 'Agent removed successfully' };
  }

  async findActiveById(id: string): Promise<AgentCostEntity | null> {
    return this.repo.findActiveById(id);
  }

  async create(dto: CreateAgentCostDto) {
    const name = dto.name.trim();
    const existing = await this.repo.findByName(name);
    if (existing) throw new ConflictException(MSG.ALREADY_EXISTS('name'));

    const item = await this.repo.save({
      name,
      percentage: dto.percentage.toFixed(2),
      is_active: dto.is_active ?? true,
    });

    return { id: item.id, message: MSG.CREATED };
  }

  async update(id: string, dto: UpdateAgentCostDto) {
    const item = await this.findById(id);

    if (dto.name !== undefined) {
      const name = dto.name.trim();
      if (name.toLowerCase() !== item.name.toLowerCase()) {
        const existing = await this.repo.findByName(name);
        if (existing && existing.id !== id) throw new ConflictException(MSG.ALREADY_EXISTS('name'));
      }
    }

    if (dto.is_active === false && item.is_active !== false) {
      await this.assertUnused(id);
    }

    const updateData: Partial<AgentCostEntity> = {};
    if (dto.name !== undefined) updateData.name = dto.name.trim();
    if (dto.percentage !== undefined) updateData.percentage = dto.percentage.toFixed(2);
    if (dto.is_active !== undefined) updateData.is_active = dto.is_active;

    await this.repo.update(id, updateData);
    return { id, message: MSG.UPDATED };
  }

  async remove(id: string) {
    await this.findById(id);
    await this.assertUnused(id);
    await this.repo.softDelete(id);
    return { message: MSG.DELETED };
  }

  private async assertUnused(id: string) {
    const count = await this.repo.countAssigned(id);
    if (count > 0) throw new ConflictException(IN_USE);
  }

  private toView(item: AgentCostEntity, assigned: AssignedTripSource[]): AgentCostView {
    return {
      id: item.id,
      name: item.name,
      percentage: Number(item.percentage),
      is_active: item.is_active,
      created_at: item.created_at,
      updated_at: item.updated_at,
      assigned_agents: assigned.map((row) => ({ id: row.id, name: row.name })),
    };
  }
}

function groupAssigned(rows: AssignedTripSource[]): Map<string, AssignedTripSource[]> {
  const grouped = new Map<string, AssignedTripSource[]>();
  for (const row of rows) {
    const list = grouped.get(row.agent_cost_id) ?? [];
    list.push(row);
    grouped.set(row.agent_cost_id, list);
  }
  return grouped;
}
