import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BaseRepository } from '@edr/api-common';
import { Company } from './entities/company.entity';
import { ListCompaniesQueryDto } from './dto/list-companies-query.dto';
import { CompanyStatsResponseDto } from './dto/company-stats-response.dto';

@Injectable()
export class CompaniesRepository extends BaseRepository<Company> {
  /**
   * A company still being filled in by its owner in the portal wizard: it was
   * self-registered (so it has an external profile) and nobody has submitted
   * onboarding yet. The row exists from the wizard's first click, carrying a
   * placeholder name + TIN, so it must not be offered up for review.
   * Staff-created companies have no external profiles and are never drafts.
   */
  private static readonly DRAFT_SQL = `(
    EXISTS (
      SELECT 1 FROM freight.external_profiles ep
      WHERE ep.company_id = company.id
        AND ep.deleted_at IS NULL
    )
    AND NOT EXISTS (
      SELECT 1 FROM freight.external_profiles ep
      WHERE ep.company_id = company.id
        AND ep.deleted_at IS NULL
        AND ep.onboarding_completed = true
    )
  )`;

  /**
   * A company waiting on a reviewer to decide an edit it submitted after being
   * approved. These rows are `status = active`, so the pending-application filter
   * can never surface them — the review queue needs its own predicate.
   */
  private static readonly PENDING_CHANGE_REQUEST_SQL = `EXISTS (
    SELECT 1 FROM freight.company_change_request ccr
    WHERE ccr.company_id = company.id
      AND ccr.status = 'pending'
      AND ccr.deleted_at IS NULL
  )`;

  /**
   * The `sortBy = 'review'` queue ordering: whatever marketing must act on
   * floats to the top. Tier 0 — submitted applications awaiting first approval
   * (drafts excluded: nothing to review yet). Tier 1 — approved customers with
   * a pending change request. Tier 2 — everyone else, drafts included.
   */
  private static readonly REVIEW_TIER_SQL = `(CASE
    WHEN company.status = 'pending' AND NOT ${CompaniesRepository.DRAFT_SQL} THEN 0
    WHEN ${CompaniesRepository.PENDING_CHANGE_REQUEST_SQL} THEN 1
    ELSE 2
  END)`;

  constructor(
    @InjectRepository(Company)
    repo: Repository<Company>,
  ) {
    super(repo);
  }

  async findByTin(tin: string): Promise<Company | null> {
    return this.repository.findOne({ where: { tin } as any });
  }

  async findByType(type: string): Promise<Company[]> {
    return this.repository.find({ where: { type } as any, order: { name: 'ASC' } });
  }

  async findByName(name: string): Promise<Company[]> {
    return this.repository
      .createQueryBuilder('company')
      .where('company.name ILIKE :name', { name: `%${name}%` })
      .getMany();
  }

  async existsByTin(tin: string): Promise<boolean> {
    const count = await this.repository.count({ where: { tin } as any });
    return count > 0;
  }

  async findPaginated(
    query: ListCompaniesQueryDto,
  ): Promise<{ items: Company[]; total: number }> {
    const {
      page = 1,
      pageSize = 20,
      search,
      type,
      kind,
      status,
      onboardingCompleted,
      hasPendingChangeRequest,
      sortBy = 'review',
      sortOrder = 'DESC',
    } = query;

    const qb = this.repository
      .createQueryBuilder('company')
      .leftJoinAndSelect('company.companyProfiles', 'companyProfiles')
      // External profiles carry onboardingCompleted, which the backoffice list
      // uses to flag customers still mid-onboarding (not yet reviewable).
      .leftJoinAndSelect('company.profiles', 'profiles')
      .where('company.deleted_at IS NULL');

    if (type) {
      qb.andWhere('company.type = :type', { type });
    }

    if (kind) {
      qb.andWhere('company.kind = :kind', { kind });
    }

    if (status) {
      qb.andWhere('company.status = :status', { status });
    }

    if (onboardingCompleted !== undefined) {
      qb.andWhere(
        onboardingCompleted
          ? `NOT ${CompaniesRepository.DRAFT_SQL}`
          : CompaniesRepository.DRAFT_SQL,
      );
    }

    if (hasPendingChangeRequest !== undefined) {
      qb.andWhere(
        hasPendingChangeRequest
          ? CompaniesRepository.PENDING_CHANGE_REQUEST_SQL
          : `NOT ${CompaniesRepository.PENDING_CHANGE_REQUEST_SQL}`,
      );
    }

    if (search) {
      const term = `%${search.trim()}%`;
      qb.andWhere(
        `(company.name ILIKE :term
          OR company.tin ILIKE :term
          OR company.email ILIKE :term
          OR EXISTS (
            SELECT 1 FROM freight.company_profiles cp
            WHERE cp.company_id = company.id
              AND cp.reference ILIKE :term
              AND cp.deleted_at IS NULL
          ))`,
        { term },
      );
    }

    // sortBy is whitelisted by @IsIn on the DTO, so it is safe to interpolate.
    if (sortBy === 'review') {
      // Queue ordering: actionable tiers first, newest first within each. The
      // tier is selected under an alias because skip/take pagination with
      // joins re-derives the ORDER BY in a subquery — a raw expression there
      // breaks, a selected alias survives.
      qb.addSelect(CompaniesRepository.REVIEW_TIER_SQL, 'review_tier')
        .orderBy('review_tier', 'ASC')
        .addOrderBy('company.createdAt', 'DESC');
    } else {
      qb.orderBy(`company.${sortBy}`, sortOrder);
    }
    const [items, total] = await qb
      // Names are not unique and createdAt can tie on bulk imports; the id
      // tiebreaker keeps paging stable instead of dropping/repeating rows.
      .addOrderBy('company.id', 'ASC')
      .skip((page - 1) * pageSize)
      .take(pageSize)
      .getManyAndCount();

    return { items, total };
  }

  async getStats(): Promise<CompanyStatsResponseDto> {
    // Drafts are counted separately rather than under `pending`: they carry
    // status=pending from creation, which would otherwise inflate the review
    // queue's KPI with customers who haven't submitted anything yet.
    const rows: { status: string; is_draft: boolean; count: string }[] =
      await this.repository
        .createQueryBuilder('company')
        .select('company.status', 'status')
        .addSelect(CompaniesRepository.DRAFT_SQL, 'is_draft')
        .addSelect('COUNT(*)', 'count')
        .where('company.deleted_at IS NULL')
        .groupBy('company.status')
        .addGroupBy(CompaniesRepository.DRAFT_SQL)
        .getRawMany();

    const pendingChanges = await this.repository
      .createQueryBuilder('company')
      .where('company.deleted_at IS NULL')
      .andWhere(CompaniesRepository.PENDING_CHANGE_REQUEST_SQL)
      .getCount();

    const map = new Map<string, number>();
    let onboarding = 0;
    let total = 0;
    for (const row of rows) {
      const count = parseInt(row.count, 10);
      total += count;
      if (row.is_draft) onboarding += count;
      else map.set(row.status, (map.get(row.status) ?? 0) + count);
    }

    return {
      total,
      active: map.get('active') ?? 0,
      pending: map.get('pending') ?? 0,
      onboarding,
      suspended: map.get('suspended') ?? 0,
      blacklisted: map.get('blacklisted') ?? 0,
      pendingChanges,
    };
  }
}
