import {
  Injectable,
  NotFoundException,
  ConflictException,
  BadRequestException,
} from "@nestjs/common";
import { CompaniesRepository } from "./companies.repository";
import { CompanyProfileRepository } from "./company-profile.repository";
import { ExternalProfileRepository } from "./external-profile.repository";
import {
  CompanyDashboardRepository,
  DashboardScope,
} from "./company-dashboard.repository";
import { MinioService } from "../minio/minio.service";
import { ETradeService } from "./services/etrade.service";
import { normalizeE164 } from "../../common/validators/is-phone-number.validator";
import { CreateCompanyDto } from "./dto/create-company.dto";
import { UpdateCompanyDto } from "./dto/update-company.dto";
import { CreateExternalProfileDto } from "./dto/create-external-profile.dto";
import { CreateCompanyWithProfileDto } from "./dto/create-company-with-profile.dto";
import { UpdateProfileDto } from "./dto/update-profile.dto";
import { ProfileResponseDto } from "./dto/profile-response.dto";
import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-response.dto";
import { ListCompaniesQueryDto } from "./dto/list-companies-query.dto";
import { CompanyStatsResponseDto } from "./dto/company-stats-response.dto";
import {
  Company,
  CompanyNationality,
  CompanyStatus,
  CompanyType,
} from "./entities/company.entity";
import { ExternalProfile } from "./entities/external-profile.entity";
import {
  BusinessLicenseFile,
  CompanyProfile,
  ProfileType,
  ProfileStatus,
} from "./entities/company-profile.entity";

export interface UserIdentity {
  userId: string;
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
}

@Injectable()
export class CompaniesService {
  constructor(
    private readonly companiesRepo: CompaniesRepository,
    private readonly companyProfilesRepo: CompanyProfileRepository,
    private readonly profilesRepo: ExternalProfileRepository,
    private readonly dashboardRepo: CompanyDashboardRepository,
    private readonly minioService: MinioService,
    private readonly etradeService: ETradeService,
  ) { }

  async createCompany(dto: CreateCompanyDto): Promise<Company> {
    const exists = await this.companiesRepo.existsByTin(dto.tin);
    if (exists) {
      throw new ConflictException(`Company with TIN ${dto.tin} already exists`);
    }
    return this.companiesRepo.create(dto);
  }

  async createCompanyWithProfile(
    identity: UserIdentity,
    dto: CreateCompanyWithProfileDto,
  ): Promise<{ company: Company; profile: ExternalProfile }> {
    if (dto.tin) {
      const exists = await this.companiesRepo.existsByTin(dto.tin);
      if (exists) {
        throw new ConflictException(
          `Company with TIN ${dto.tin} already exists`,
        );
      }
    }

    const existingProfile = await this.profilesRepo.findByEmail(identity.email);
    if (existingProfile) {
      throw new ConflictException(
        `Profile with email ${identity.email} already exists`,
      );
    }

    const company = await this.companiesRepo.create({
      name: dto.companyName,
      type: dto.companyType,
      tin: dto.tin ?? "",
      vatNumber: dto.vatNumber ?? null,
      fanNumber: dto.fanNumber ?? null,
      country: dto.companyLocation ?? "Ethiopia",
      address: dto.companyAddress ?? null,
      phone: normalizeE164(dto.companyPhone) ?? null,
      email: dto.companyEmail ?? null,
      attributes: dto.attributes ?? null,
    });

    // Default active mode from the chosen role(s): importer wins when both are
    // picked, otherwise the first allowed type chosen.
    const allowedTypes = this.getProfileTypeForCompanyType(company.type);
    const chosenTypes = (dto.companyProfiles ?? [])
      .map((p) => p.type)
      .filter((t) => allowedTypes.includes(t));
    const activeProfileType =
      chosenTypes.find((t) => t === ProfileType.importer) ??
      chosenTypes[0] ??
      allowedTypes[0] ??
      null;

    const profile = await this.profilesRepo.create({
      userId: identity.userId,
      companyId: company.id,
      firstName: identity.firstName,
      lastName: identity.lastName,
      email: identity.email,
      phone: normalizeE164(identity.phone) ?? identity.phone,
      jobTitle: dto.jobTitle ?? null,
      isPrimaryContact: dto.isPrimaryContact ?? true,
      activeProfileType,
      onboardingStep: "company",
    });

    // Persist the operational role(s) chosen during onboarding. Types are
    // already constrained to the company type on the client; any that don't
    // match are skipped defensively rather than failing the whole signup.
    if (dto.companyProfiles?.length) {
      const allowedTypes = this.getProfileTypeForCompanyType(company.type);
      for (const input of dto.companyProfiles) {
        if (!allowedTypes.includes(input.type)) continue;
        const existing = await this.companyProfilesRepo.findByType(
          company.id,
          input.type,
        );
        if (existing) continue;
        const reference = await this.companyProfilesRepo.generateReference(
          input.type,
        );
        await this.companyProfilesRepo.create({
          companyId: company.id,
          type: input.type,
          reference,
          businessLicense: input.businessLicense ?? null,
          status: ProfileStatus.Active,
        });
      }
      company.companyProfiles = await this.companyProfilesRepo.findByCompanyId(
        company.id,
      );
    }

    return { company, profile };
  }

  async listCompanies(
    query: ListCompaniesQueryDto,
  ): Promise<{ items: Company[]; total: number }> {
    return this.companiesRepo.findPaginated(query);
  }

  async getCompanyStats(): Promise<CompanyStatsResponseDto> {
    return this.companiesRepo.getStats();
  }

  /**
   * Begin onboarding: create a DRAFT company + the user's external profile + the
   * chosen operational role(s) up front, so every subsequent wizard step can
   * save incrementally (PATCH /profile, /onboarding-step) against existing rows.
   *
   * Idempotent: if the user already has a profile, returns it unchanged (only
   * adding any newly-chosen roles). The draft company carries a placeholder TIN
   * (the real one is filled on the Company Information step) and stays
   * status=pending / onboardingCompleted=false until the wizard finishes.
   */
  async startOnboarding(
    identity: UserIdentity,
    companyType: CompanyType,
    roles: ProfileType[],
    nationality?: CompanyNationality,
  ): Promise<{ profile: ExternalProfile; company: Company }> {
    // Already started — reuse the existing draft, just ensure roles exist and
    // keep the nationality up to date if it was (re)selected.
    const existing = await this.profilesRepo.findByUserId(identity.userId);
    if (existing) {
      const companyId = existing.company?.id ?? existing.companyId;
      await this.ensureCompanyProfiles(companyId, companyType, roles);
      if (nationality) {
        await this.companiesRepo.update(companyId, { nationality });
      }
      return this.getCompanyInfoByUserId(identity.userId);
    }

    // A profile may exist for the same email under a different IAM id — block
    // duplicates as the final create does.
    const byEmail = await this.profilesRepo.findByEmail(identity.email);
    if (byEmail) {
      throw new ConflictException(
        `Profile with email ${identity.email} already exists`,
      );
    }

    const allowedTypes = this.getProfileTypeForCompanyType(companyType);
    const chosenTypes = roles.filter((t) => allowedTypes.includes(t));
    const activeProfileType =
      chosenTypes.find((t) => t === ProfileType.importer) ??
      chosenTypes[0] ??
      allowedTypes[0] ??
      null;

    const company = await this.companiesRepo.create({
      name: identity.firstName
        ? `${identity.firstName}'s company`
        : "New company",
      type: companyType,
      tin: await this.generateDraftTin(),
      country: "Ethiopia",
      nationality: nationality ?? CompanyNationality.Ethiopian,
      status: CompanyStatus.Pending,
    });

    await this.profilesRepo.create({
      userId: identity.userId,
      companyId: company.id,
      firstName: identity.firstName,
      lastName: identity.lastName,
      email: identity.email,
      phone: normalizeE164(identity.phone) ?? identity.phone,
      isPrimaryContact: true,
      activeProfileType,
      onboardingStep: "company",
      onboardingCompleted: false,
    });

    await this.ensureCompanyProfiles(company.id, companyType, chosenTypes);

    return this.getCompanyInfoByUserId(identity.userId);
  }

  /** Create any of the requested operational profiles that don't exist yet. */
  private async ensureCompanyProfiles(
    companyId: string,
    companyType: CompanyType,
    roles: ProfileType[],
  ): Promise<void> {
    const allowedTypes = this.getProfileTypeForCompanyType(companyType);
    for (const type of roles) {
      if (!allowedTypes.includes(type)) continue;
      const existing = await this.companyProfilesRepo.findByType(
        companyId,
        type,
      );
      if (existing) continue;
      const reference = await this.companyProfilesRepo.generateReference(type);
      await this.companyProfilesRepo.create({
        companyId,
        type,
        reference,
        status: ProfileStatus.Active,
      });
    }
  }

  /**
   * A unique 10-char placeholder TIN for a draft company (the column is
   * NOT NULL + unique). Overwritten with the real TIN on the company step.
   */
  private async generateDraftTin(): Promise<string> {
    for (let i = 0; i < 10; i++) {
      const candidate =
        "D" +
        Math.floor(Math.random() * 1_000_000_000)
          .toString()
          .padStart(9, "0");
      if (!(await this.companiesRepo.existsByTin(candidate))) return candidate;
    }
    // Extremely unlikely; fall back to a timestamp-derived value.
    return ("D" + Date.now().toString()).slice(0, 10);
  }

  async findAllCompanies(): Promise<Company[]> {
    return this.companiesRepo.findAll({ order: { name: "ASC" } });
  }

  async findCompanyById(id: string): Promise<Company> {
    const company = await this.companiesRepo.findById(id);
    if (!company) throw new NotFoundException(`Company ${id} not found`);
    company.companyProfiles = await this.companyProfilesRepo.findByCompanyId(id);
    return company;
  }

  async getCompanyInfoByUserId(
    userId: string,
  ): Promise<{ profile: ExternalProfile; company: Company }> {
    const profile = await this.profilesRepo.findByUserId(userId);
    if (!profile)
      throw new NotFoundException(`Profile for user ${userId} not found`);

    const company = profile.company;
    if (!company)
      throw new NotFoundException(
        `Company for profile ${profile.id} not found`,
      );

    company.companyProfiles = await this.companyProfilesRepo.findByCompanyId(
      company.id,
    );

    return { profile, company };
  }

  /**
   * Dashboard KPIs for the portal home (MyPortalPage), aggregated from the
   * current user's company bookings. All figures are scoped to that company.
   *
   * Note: delivered/spend/volume all derive from the bookings table — there is
   * no separate data source for them. On-time delivery rate is replaced by
   * completion rate (delivered ÷ committed): the schema has no ETA /
   * promised-delivery date, so on-time cannot be computed.
   *
   * Period attribution uses booking.created_at: there is no delivery-date
   * column, so "delivered YTD" counts bookings created this year that reached a
   * delivered/completed status.
   */
  async getDashboardSummary(
    userId: string,
    companyProfileId?: string,
  ): Promise<DashboardSummaryResponseDto> {
    // A user without a company profile has no bookings — return an empty summary
    // rather than 404, so the portal home still renders.
    const profile = await this.profilesRepo.findByUserId(userId);
    const companyId = profile?.company?.id ?? profile?.companyId ?? null;
    if (!companyId) return this.emptyDashboardSummary();

    // Company-wide by default (all services' data). An optional companyProfileId
    // (from the per-page service filter) narrows to one operational profile —
    // but only after we confirm it belongs to this user's company, since the
    // dashboard scope has no company guard at the repository layer.
    let scope: DashboardScope = { companyId };
    if (companyProfileId) {
      const owned = await this.companyProfilesRepo.findByCompanyId(companyId);
      if (owned.some((p) => p.id === companyProfileId)) {
        scope = { companyProfileId };
      }
    }

    const now = new Date();
    const yearStart = new Date(now.getFullYear(), 0, 1);
    const prevYearStart = new Date(now.getFullYear() - 1, 0, 1);
    // Same point in the previous year, so YoY compares like-for-like windows.
    const prevYearToDate = new Date(
      prevYearStart.getTime() + (now.getTime() - yearStart.getTime()),
    );

    const [
      deliveredThis,
      committedThis,
      spendThisByCcy,
      spendPrevByCcy,
      tonnageThis,
      tonnagePrev,
      monthlyRows,
    ] = await Promise.all([
      this.dashboardRepo.countDelivered(scope, yearStart, now),
      this.dashboardRepo.countCommitted(scope, yearStart, now),
      this.dashboardRepo.sumPaidSpendByCurrency(scope, yearStart, now),
      this.dashboardRepo.sumPaidSpendByCurrency(
        scope,
        prevYearStart,
        prevYearToDate,
      ),
      this.dashboardRepo.sumCommittedTonnage(scope, yearStart, now),
      this.dashboardRepo.sumCommittedTonnage(
        scope,
        prevYearStart,
        prevYearToDate,
      ),
      this.dashboardRepo.monthlyCommittedTonnage(
        scope,
        this.monthsAgo(now, 5),
        now,
      ),
    ]);

    // Spend can span currencies; report the dominant one (prefer ETB on ties).
    const spend = this.pickCurrencyTotal(spendThisByCcy);
    const spendPrev =
      spendPrevByCcy.find((c) => c.currency === spend.currency)?.total ?? 0;

    return {
      deliveredCount: deliveredThis,
      // Share of committed bookings that reached delivered/completed.
      completionRate:
        committedThis > 0
          ? Math.round((deliveredThis / committedThis) * 100)
          : 0,
      spendYtd: spend.total,
      spendCurrency: spend.currency,
      spendYtdChangePct: this.changePct(spend.total, spendPrev),
      freightVolume: {
        totalTonnes: Math.round(tonnageThis),
        totalValue: spend.total,
        currency: spend.currency,
        ytdChangePct: this.changePct(tonnageThis, tonnagePrev),
        monthly: this.buildMonthlySeries(now, monthlyRows),
      },
    };
  }

  private emptyDashboardSummary(): DashboardSummaryResponseDto {
    const now = new Date();
    return {
      deliveredCount: 0,
      completionRate: 0,
      spendYtd: 0,
      spendCurrency: "ETB",
      spendYtdChangePct: 0,
      freightVolume: {
        totalTonnes: 0,
        totalValue: 0,
        currency: "ETB",
        ytdChangePct: 0,
        monthly: this.buildMonthlySeries(now, []),
      },
    };
  }

  /** First day of the month `n` months before `from`. */
  private monthsAgo(from: Date, n: number): Date {
    return new Date(from.getFullYear(), from.getMonth() - n, 1);
  }

  /** Pick the currency with the largest total, preferring ETB on ties / when empty. */
  private pickCurrencyTotal(totals: { currency: string; total: number }[]): {
    currency: string;
    total: number;
  } {
    if (totals.length === 0) return { currency: "ETB", total: 0 };
    return totals.reduce((best, cur) => (cur.total > best.total ? cur : best));
  }

  /** Percentage change vs a prior value, rounded; 0 when there is no prior base. */
  private changePct(current: number, previous: number): number {
    if (previous <= 0) return 0;
    return Math.round(((current - previous) / previous) * 100);
  }

  /** Build a fixed 6-month tonnage series ending on `now`, zero-filling gaps. */
  private buildMonthlySeries(
    now: Date,
    rows: { year: number; month: number; tonnes: number }[],
  ): { month: string; tonnes: number }[] {
    const labels = [
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec",
    ];
    const byKey = new Map(rows.map((r) => [`${r.year}-${r.month}`, r.tonnes]));
    const series: { month: string; tonnes: number }[] = [];
    for (let i = 5; i >= 0; i--) {
      const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
      const key = `${d.getFullYear()}-${d.getMonth() + 1}`;
      series.push({
        month: labels[d.getMonth()],
        tonnes: Math.round(byKey.get(key) ?? 0),
      });
    }
    return series;
  }

  async updateCompany(id: string, dto: UpdateCompanyDto): Promise<Company> {
    await this.findCompanyById(id);
    const updated = await this.companiesRepo.update(id, dto);
    if (!updated) throw new NotFoundException(`Company ${id} not found`);
    return updated;
  }

  async updateProfile(
    userId: string,
    dto: UpdateProfileDto,
  ): Promise<ProfileResponseDto> {
    const { profile, company } = await this.getCompanyInfoByUserId(userId);

    const companyUpdates: Record<string, any> = {};
    const attrUpdates: Record<string, any> = { ...(company.attributes ?? {}) };

    if (dto.nationality !== undefined)
      companyUpdates.nationality = dto.nationality;
    if (dto.companyName !== undefined) companyUpdates.name = dto.companyName;
    if (dto.companyEmail !== undefined) companyUpdates.email = dto.companyEmail;
    if (dto.companyPhone !== undefined)
      companyUpdates.phone = normalizeE164(dto.companyPhone);
    if (dto.companyLocation !== undefined)
      companyUpdates.country = dto.companyLocation;
    if (dto.companyAddress !== undefined)
      companyUpdates.address = dto.companyAddress;
    if (dto.tin !== undefined && dto.tin !== company.tin) {
      // Reject a TIN already taken by a different company (the user's own draft
      // placeholder is fine to overwrite).
      const owner = await this.companiesRepo.findByTin(dto.tin);
      if (owner && owner.id !== company.id) {
        throw new ConflictException(
          `This TIN (${dto.tin}) is already registered to another company. Please check the number and try again.`,
        );
      }
      companyUpdates.tin = dto.tin;
    }
    if (dto.vatNumber !== undefined) companyUpdates.vatNumber = dto.vatNumber;
    if (dto.fanNumber !== undefined) {
      companyUpdates.fanNumber = dto.fanNumber;
    }

    if (dto.contactPersonName !== undefined)
      attrUpdates.contactPersonName = dto.contactPersonName;
    if (dto.contactPersonPosition !== undefined)
      attrUpdates.contactPersonPosition = dto.contactPersonPosition;
    if (dto.contactPersonEmail !== undefined)
      attrUpdates.contactPersonEmail = dto.contactPersonEmail;
    if (dto.contactPersonPhone !== undefined)
      attrUpdates.contactPersonPhone = normalizeE164(dto.contactPersonPhone);
    if (dto.generalManagerName !== undefined)
      attrUpdates.generalManagerName = dto.generalManagerName;
    if (dto.generalManagerEmail !== undefined)
      attrUpdates.generalManagerEmail = dto.generalManagerEmail;
    if (dto.generalManagerPhone !== undefined)
      attrUpdates.generalManagerPhone = normalizeE164(dto.generalManagerPhone);
    if (dto.poaName !== undefined) attrUpdates.poaName = dto.poaName;
    if (dto.poaPhone !== undefined)
      attrUpdates.poaPhone = normalizeE164(dto.poaPhone);
    if (dto.poaEmail !== undefined) attrUpdates.poaEmail = dto.poaEmail;
    if (dto.poaLocation !== undefined)
      attrUpdates.poaLocation = dto.poaLocation;
    if (dto.poaAddress !== undefined) attrUpdates.poaAddress = dto.poaAddress;

    if (dto.licenceNumber !== undefined)
      companyUpdates.licenceNumber = dto.licenceNumber;
    if (dto.statusDescription !== undefined)
      companyUpdates.statusDescription = dto.statusDescription;
    if (dto.dateRegistered !== undefined)
      companyUpdates.dateRegistered = dto.dateRegistered;
    if (dto.renewedFrom !== undefined)
      companyUpdates.renewedFrom = dto.renewedFrom;
    if (dto.renewalDate !== undefined)
      companyUpdates.renewalDate = dto.renewalDate;
    if (dto.renewedTo !== undefined) companyUpdates.renewedTo = dto.renewedTo;
    if (dto.region !== undefined) companyUpdates.region = dto.region;
    if (dto.zone !== undefined) companyUpdates.zone = dto.zone;
    if (dto.woreda !== undefined) companyUpdates.woreda = dto.woreda;
    if (dto.kebele !== undefined) companyUpdates.kebele = dto.kebele;
    if (dto.houseNo !== undefined) companyUpdates.houseNo = dto.houseNo;
    if (dto.etradePhone !== undefined)
      companyUpdates.etradePhone = normalizeE164(dto.etradePhone);

    companyUpdates.attributes = attrUpdates;

    const updated = await this.companiesRepo.update(company.id, companyUpdates);
    if (!updated)
      throw new NotFoundException(`Company ${company.id} not found`);
    return new ProfileResponseDto(profile, updated);
  }

  async deleteCompany(id: string): Promise<void> {
    await this.findCompanyById(id);
    await this.companiesRepo.softDelete(id);
  }

  async createProfile(dto: CreateExternalProfileDto): Promise<ExternalProfile> {
    await this.findCompanyById(dto.companyId);

    const existing = await this.profilesRepo.findByEmail(dto.email);
    if (existing) {
      throw new ConflictException(
        `Profile with email ${dto.email} already exists`,
      );
    }

    return this.profilesRepo.create(dto);
  }

  async findProfileByUserId(userId: string): Promise<ExternalProfile> {
    const profile = await this.profilesRepo.findByUserId(userId);
    if (!profile)
      throw new NotFoundException(`Profile for user ${userId} not found`);
    return profile;
  }

  async findProfilesByCompany(companyId: string): Promise<ExternalProfile[]> {
    return this.profilesRepo.findByCompanyId(companyId);
  }

  private getProfileTypeForCompanyType(companyType: string): ProfileType[] {
    switch (companyType) {
      case "customer":
        // A customer can operate as an importer and/or exporter, and may also
        // add a freight-forwarder service profile under the same company.
        return [
          ProfileType.importer,
          ProfileType.exporter,
          ProfileType.freightForwarder,
        ];
      case "freight_forwarder":
        return [ProfileType.freightForwarder];
      case "dj_freight_forwarder":
        return [ProfileType.djFreightForwarder];
      case "transporter":
        return [ProfileType.transporter];
      default:
        return [];
    }
  }

  async setCompanyProfileStatus(
    profileId: string,
    status: ProfileStatus,
  ): Promise<CompanyProfile> {
    const updated = await this.companyProfilesRepo.updateStatus(
      profileId,
      status,
    );
    if (!updated)
      throw new NotFoundException(`Company profile ${profileId} not found`);
    return updated;
  }

  async createCompanyProfile(
    companyId: string,
    profileType?: ProfileType,
  ): Promise<CompanyProfile> {
    const company = await this.findCompanyById(companyId);

    const allowedTypes = this.getProfileTypeForCompanyType(company.type);

    const type = profileType ?? allowedTypes[0];
    if (!allowedTypes.includes(type)) {
      throw new BadRequestException(
        `Profile type "${type}" is not allowed for company type "${company.type}"`,
      );
    }

    const existing = await this.companyProfilesRepo.findByType(companyId, type);
    if (existing) {
      throw new ConflictException(
        `Company already has a ${type} profile (${existing.reference})`,
      );
    }

    const reference = await this.companyProfilesRepo.generateReference(type);

    return this.companyProfilesRepo.create({
      companyId,
      type,
      reference,
      status: ProfileStatus.Active,
    });
  }

  async createDefaultProfilesForCompany(
    companyId: string,
  ): Promise<CompanyProfile[]> {
    const company = await this.findCompanyById(companyId);
    const types = this.getProfileTypeForCompanyType(company.type);

    const profiles: CompanyProfile[] = [];
    for (const type of types) {
      const existing = await this.companyProfilesRepo.findByType(
        companyId,
        type,
      );
      if (!existing) {
        profiles.push(await this.createCompanyProfile(companyId, type));
      }
    }

    if (profiles.length === 0) {
      throw new BadRequestException(
        `Company of type "${company.type}" must have at least one operational profile`,
      );
    }

    return profiles;
  }

  /**
   * Add operational profile(s) to the current user's company (portal settings).
   * Add-only and idempotent: each requested type must be allowed for the
   * company's type, profiles that already exist are skipped (not re-created or
   * rejected), and the full updated list is returned.
   */
  async addCompanyProfilesForUser(
    userId: string,
    types: ProfileType[],
  ): Promise<CompanyProfile[]> {
    const profile = await this.profilesRepo.findByUserId(userId);
    if (!profile)
      throw new NotFoundException(`Profile for user ${userId} not found`);

    const companyId = profile.company?.id ?? profile.companyId;
    const company = await this.findCompanyById(companyId);
    const allowedTypes = this.getProfileTypeForCompanyType(company.type);

    for (const type of types) {
      if (!allowedTypes.includes(type)) {
        throw new BadRequestException(
          `Profile type "${type}" is not allowed for company type "${company.type}"`,
        );
      }

      const existing = await this.companyProfilesRepo.findByType(
        companyId,
        type,
      );
      if (existing) continue;

      const reference = await this.companyProfilesRepo.generateReference(type);
      await this.companyProfilesRepo.create({
        companyId,
        type,
        reference,
        status: ProfileStatus.Active,
      });
    }

    return this.companyProfilesRepo.findByCompanyId(companyId);
  }

  /**
   * Create a single operational profile for the current user's company and
   * make it the active mode in the same call. Powers the header "Switch to
   * Exporter/Importer" flow when the target profile doesn't exist yet.
   */
  async createCompanyProfileForUser(
    userId: string,
    type: ProfileType,
    businessLicense?: string,
  ): Promise<CompanyProfile> {
    const profile = await this.profilesRepo.findByUserId(userId);
    if (!profile)
      throw new NotFoundException(`Profile for user ${userId} not found`);

    const companyId = profile.company?.id ?? profile.companyId;
    const company = await this.findCompanyById(companyId);
    const allowedTypes = this.getProfileTypeForCompanyType(company.type);
    if (!allowedTypes.includes(type)) {
      throw new BadRequestException(
        `Profile type "${type}" is not allowed for company type "${company.type}"`,
      );
    }

    let created = await this.companyProfilesRepo.findByType(companyId, type);
    if (!created) {
      const reference = await this.companyProfilesRepo.generateReference(type);
      created = await this.companyProfilesRepo.create({
        companyId,
        type,
        reference,
        businessLicense: businessLicense ?? null,
        status: ProfileStatus.Active,
      });
    }

    await this.profilesRepo.update(profile.id, { activeProfileType: type });

    return created;
  }

  /**
   * Switch the user's active operational mode. The target profile must already
   * exist — clients create it first via createCompanyProfileForUser.
   */
  async setActiveMode(
    userId: string,
    type: ProfileType,
  ): Promise<{ profile: ExternalProfile; company: Company }> {
    const profile = await this.profilesRepo.findByUserId(userId);
    if (!profile)
      throw new NotFoundException(`Profile for user ${userId} not found`);

    const companyId = profile.company?.id ?? profile.companyId;
    const company = await this.findCompanyById(companyId);

    const allowedTypes = this.getProfileTypeForCompanyType(company.type);
    if (!allowedTypes.includes(type)) {
      throw new BadRequestException(
        `Profile type "${type}" is not allowed for company type "${company.type}"`,
      );
    }

    const existing = await this.companyProfilesRepo.findByType(companyId, type);
    if (!existing) {
      throw new ConflictException(
        `No ${type} profile exists yet — create it before switching`,
      );
    }

    await this.profilesRepo.update(profile.id, { activeProfileType: type });

    return this.getCompanyInfoByUserId(userId);
  }

  async setOnboardingStep(userId: string, step: string): Promise<void> {
    const profile = await this.profilesRepo.findByUserId(userId);
    if (!profile)
      throw new NotFoundException(`Profile for user ${userId} not found`);
    await this.profilesRepo.update(profile.id, { onboardingStep: step });
  }

  async markOnboardingComplete(
    userId: string,
  ): Promise<{ profile: ExternalProfile; company: Company }> {
    const profile = await this.profilesRepo.findByUserId(userId);
    if (!profile)
      throw new NotFoundException(`Profile for user ${userId} not found`);

    const companyId = profile.company?.id ?? profile.companyId;
    const company = await this.findCompanyById(companyId);

    // Guard against finishing on a still-draft company (TIN never filled in).
    if (!company.tin || company.tin.startsWith("D")) {
      throw new BadRequestException(
        "Company information is incomplete — please fill in your company details before finishing.",
      );
    }

    // Every operational profile must have at least one business-license file
    // (stored directly on the profile).
    const profiles = await this.companyProfilesRepo.findByCompanyId(companyId);
    for (const cp of profiles) {
      if (!cp.businessLicenseFiles || cp.businessLicenseFiles.length === 0) {
        throw new BadRequestException(
          `Please upload a business license for your ${cp.type.replace(/_/g, " ")} profile before finishing.`,
        );
      }
    }

    await this.profilesRepo.update(profile.id, {
      onboardingCompleted: true,
      onboardingStep: "done",
    });
    // Awaiting backoffice approval — stays Pending until an admin activates it.
    await this.companiesRepo.update(companyId, {
      status: CompanyStatus.Pending,
    });
    return this.getCompanyInfoByUserId(userId);
  }

  /**
   * Authorize and resolve a company_profile that must belong to the current
   * user's company — used before accepting/returning its license files.
   */
  async resolveOwnedProfile(
    userId: string,
    profileId: string,
  ): Promise<CompanyProfile> {
    const { company } = await this.getCompanyInfoByUserId(userId);
    const owned = (company.companyProfiles ?? []).find(
      (p) => p.id === profileId,
    );
    if (!owned) {
      throw new NotFoundException(`Profile ${profileId} not found`);
    }
    return owned;
  }

  /**
   * Upload business-license document(s) and store them directly on the company
   * profile (multi-file). Bytes go to object storage; only metadata/URLs are
   * persisted on the profile — intentionally not via the FileRecord file model.
   * New files are appended to any already present. Returns the full list.
   */
  async uploadProfileLicenseFiles(
    userId: string,
    profileId: string,
    files: Express.Multer.File[],
  ): Promise<BusinessLicenseFile[]> {
    const profile = await this.resolveOwnedProfile(userId, profileId);

    const uploaded: BusinessLicenseFile[] = [];
    for (const file of files) {
      const objectName = `company_profiles/${profileId}/${Date.now()}_${file.originalname}`;
      const url = await this.minioService.uploadFile(
        objectName,
        file.buffer,
        file.mimetype,
      );
      uploaded.push({
        name: file.originalname,
        url,
        size: file.size,
        mimeType: file.mimetype,
      });
    }

    const next = [...(profile.businessLicenseFiles ?? []), ...uploaded];
    await this.companyProfilesRepo.update(profileId, {
      businessLicenseFiles: next,
    });
    return next;
  }

  /** The business-license files stored on a single company profile. */
  async listProfileLicenseFiles(
    userId: string,
    profileId: string,
  ): Promise<BusinessLicenseFile[]> {
    const profile = await this.resolveOwnedProfile(userId, profileId);
    return profile.businessLicenseFiles ?? [];
  }

  /**
   * Onboarding documents stored on a company profile, fetched by profile id.
   * Internal helper (no ownership check) used when a booking reuses the active
   * profile's onboarding documents. Returns [] when the profile is unknown.
   */
  async getProfileOnboardingFiles(
    profileId: string,
  ): Promise<BusinessLicenseFile[]> {
    const profile = await this.companyProfilesRepo.findById(profileId);
    return profile?.businessLicenseFiles ?? [];
  }

  /**
   * Resolve which company_profile a new booking belongs to, from the company
   * and the booking's trade direction. IMPORT → importer profile, EXPORT →
   * exporter profile; for DOMESTIC or a forwarder/single-profile company (or
   * when the natural profile doesn't exist) it falls back to the user's active
   * profile, then the company's first profile. Returns null when the company
   * has no profiles at all.
   */
  async resolveCompanyProfileIdForBooking(
    companyId: string,
    tradeDirection: string,
    fallbackType?: ProfileType | null,
  ): Promise<string | null> {
    const profiles = await this.companyProfilesRepo.findByCompanyId(companyId);
    if (profiles.length === 0) return null;

    const naturalType =
      tradeDirection === "IMPORT"
        ? ProfileType.importer
        : tradeDirection === "EXPORT"
          ? ProfileType.exporter
          : null;

    const byType = (type?: ProfileType | null) =>
      type ? profiles.find((p) => p.type === type) : undefined;

    const match = byType(naturalType) ?? byType(fallbackType) ?? profiles[0];
    return match?.id ?? null;
  }

  /**
   * Resolve the company_profile a customer's data should be scoped to, from
   * their persisted active mode. Returns null when nothing can be resolved
   * (not onboarded yet) so callers can fall back to company-level scoping.
   */
  async resolveActiveCompanyProfileId(userId: string): Promise<string | null> {
    try {
      const { profile, company } = await this.getCompanyInfoByUserId(userId);
      const type = profile.activeProfileType;
      if (!type) return null;
      const match = company.companyProfiles?.find((p) => p.type === type);
      return match?.id ?? null;
    } catch {
      return null;
    }
  }

  async fetchETradeData(tin: string) {
    const { businessInfo } = await this.etradeService.resolveCompanyData(tin);
    if (!businessInfo) {
      throw new BadRequestException(
        "No business license found for this TIN. Please check the number and try again.",
      );
    }
    return this.etradeService.extractRegistrationData(businessInfo);
  }
}
