import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { PrismaService } from "@bff/infra/database/prisma.service.js";
import { SftpClientService } from "@bff/integrations/sftp/sftp-client.service.js";
import { SimValidationService } from "../sim-validation.service.js";
import { SimCallHistoryParserService } from "./sim-call-history-parser.service.js";
import { SimCallHistoryFormatterService } from "./sim-call-history-formatter.service.js";
import type {
  SimDomesticCallHistoryResponse,
  SimInternationalCallHistoryResponse,
  SimSmsHistoryResponse,
} from "@customer-portal/domain/sim";

// Re-export types for consumers
export type {
  DomesticCallRecord,
  InternationalCallRecord,
  SmsRecord,
} from "./sim-call-history-parser.service.js";

// SmsType enum to match Prisma schema
type SmsType = "DOMESTIC" | "INTERNATIONAL";

export interface CallHistoryPagination {
  page: number;
  limit: number;
  total: number;
  totalPages: number;
}

export interface CallHistoryQueryOptions {
  userId: string;
  subscriptionId: number;
  month?: string | undefined;
  page?: number | undefined;
  limit?: number | undefined;
}

/**
 * Service for managing SIM call history data.
 * Coordinates importing data from SFTP and querying stored history.
 */
@Injectable()
export class SimCallHistoryService {
  // eslint-disable-next-line max-params -- NestJS DI requires individual constructor parameters
  constructor(
    private readonly prisma: PrismaService,
    private readonly sftp: SftpClientService,
    private readonly simValidation: SimValidationService,
    private readonly parser: SimCallHistoryParserService,
    private readonly formatter: SimCallHistoryFormatterService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Import call history from SFTP for a specific month
   */
  async importCallHistory(yearMonth: string): Promise<{
    domestic: number;
    international: number;
    sms: number;
  }> {
    const month = this.formatter.normalizeMonth(yearMonth);

    this.logger.log(`Starting call history import for ${month}`);

    await this.prisma.simHistoryImport.deleteMany({ where: { month } });
    this.logger.log(`Cleared existing import record for ${month}`);

    const { domesticCount, internationalCount } = await this.importTalkDetail(yearMonth, month);
    const smsCount = await this.importSmsDetail(yearMonth, month);

    await this.recordImport({ yearMonth, month, domesticCount, internationalCount, smsCount });

    return { domestic: domesticCount, international: internationalCount, sms: smsCount };
  }

  private async importTalkDetail(
    yearMonth: string,
    month: string
  ): Promise<{ domesticCount: number; internationalCount: number }> {
    try {
      const talkContent = await this.sftp.downloadTalkDetail(yearMonth);
      const parsed = this.parser.parseTalkDetailCsv(talkContent, month);

      this.logger.log(`Parsed talk detail`, {
        domestic: parsed.domestic.length,
        international: parsed.international.length,
        skipped: parsed.skipped,
        errors: parsed.errors,
      });

      const domesticCount = await this.processDomesticRecords(parsed.domestic);
      const internationalCount = await this.processInternationalRecords(parsed.international);

      this.logger.log(
        `Imported ${domesticCount} domestic and ${internationalCount} international calls`
      );

      return { domesticCount, internationalCount };
    } catch (error) {
      this.logger.error(`Failed to import talk detail`, { error, yearMonth });
      return { domesticCount: 0, internationalCount: 0 };
    }
  }

  private async processDomesticRecords(
    records: Array<{
      account: string;
      month: string;
      callDate: Date;
      callTime: string;
      calledTo: string;
      location: string | null;
      durationSec: number;
      chargeYen: number;
    }>
  ): Promise<number> {
    return this.processInBatches(
      records,
      50,
      async record =>
        this.prisma.simCallHistoryDomestic.upsert({
          where: {
            account_callDate_callTime_calledTo: {
              account: record.account,
              callDate: record.callDate,
              callTime: record.callTime,
              calledTo: record.calledTo,
            },
          },
          update: {
            location: record.location,
            durationSec: record.durationSec,
            chargeYen: record.chargeYen,
          },
          create: record,
        }),
      (record, error) => this.logger.warn("Failed to store domestic call record", { record, error })
    );
  }

  private async processInternationalRecords(
    records: Array<{
      account: string;
      month: string;
      callDate: Date;
      startTime: string;
      stopTime: string | null;
      calledTo: string;
      country: string | null;
      durationSec: number;
      chargeYen: number;
    }>
  ): Promise<number> {
    return this.processInBatches(
      records,
      50,
      async record =>
        this.prisma.simCallHistoryInternational.upsert({
          where: {
            account_callDate_startTime_calledTo: {
              account: record.account,
              callDate: record.callDate,
              startTime: record.startTime,
              calledTo: record.calledTo,
            },
          },
          update: {
            stopTime: record.stopTime,
            country: record.country,
            durationSec: record.durationSec,
            chargeYen: record.chargeYen,
          },
          create: record,
        }),
      (record, error) =>
        this.logger.warn("Failed to store international call record", { record, error })
    );
  }

  private async importSmsDetail(yearMonth: string, month: string): Promise<number> {
    try {
      const smsContent = await this.sftp.downloadSmsDetail(yearMonth);
      const parsed = this.parser.parseSmsDetailCsv(smsContent, month);

      this.logger.log(`Parsed SMS detail`, {
        records: parsed.records.length,
        skipped: parsed.skipped,
        errors: parsed.errors,
      });

      const smsCount = await this.processInBatches(
        parsed.records,
        50,
        async record =>
          this.prisma.simSmsHistory.upsert({
            where: {
              account_smsDate_smsTime_sentTo: {
                account: record.account,
                smsDate: record.smsDate,
                smsTime: record.smsTime,
                sentTo: record.sentTo,
              },
            },
            update: { smsType: record.smsType },
            create: record,
          }),
        (record, error) => this.logger.warn("Failed to store SMS record", { record, error })
      );

      this.logger.log(`Imported ${smsCount} SMS records`);
      return smsCount;
    } catch (error) {
      this.logger.error(`Failed to import SMS detail`, { error, yearMonth });
      return 0;
    }
  }

  private async recordImport(options: {
    yearMonth: string;
    month: string;
    domesticCount: number;
    internationalCount: number;
    smsCount: number;
  }): Promise<void> {
    const { yearMonth, month, domesticCount, internationalCount, smsCount } = options;
    const talkFile = this.sftp.getTalkDetailFileName(yearMonth);
    const smsFile = this.sftp.getSmsDetailFileName(yearMonth);
    const talkRecords = domesticCount + internationalCount;

    await this.prisma.simHistoryImport.upsert({
      where: { month },
      update: {
        talkFile,
        smsFile,
        talkRecords,
        smsRecords: smsCount,
        status: "completed",
        importedAt: new Date(),
      },
      create: {
        month,
        talkFile,
        smsFile,
        talkRecords,
        smsRecords: smsCount,
        status: "completed",
      },
    });
  }

  /**
   * Get domestic call history for a user's SIM
   */
  async getDomesticCallHistory(
    options: CallHistoryQueryOptions
  ): Promise<SimDomesticCallHistoryResponse> {
    const { userId, subscriptionId, month, page = 1, limit = 50 } = options;
    await this.simValidation.validateSimSubscription(userId, subscriptionId);
    // Dev/testing mode: call history data is currently sourced from a fixed account.
    // TODO: Replace with the validated subscription account once call history data is available per user.
    const account = "08077052946";

    const targetMonth = month || this.formatter.getDefaultMonth();

    const [calls, total] = await Promise.all([
      this.prisma.simCallHistoryDomestic.findMany({
        where: { account, month: targetMonth },
        orderBy: [{ callDate: "desc" }, { callTime: "desc" }],
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.simCallHistoryDomestic.count({
        where: { account, month: targetMonth },
      }),
    ]);

    return {
      calls: calls.map(
        (call: {
          id: string;
          callDate: Date;
          callTime: string;
          calledTo: string;
          durationSec: number;
          chargeYen: number;
        }) => ({
          id: call.id,
          date: call.callDate.toISOString().split("T")[0] ?? "",
          time: this.formatter.formatTime(call.callTime),
          calledTo: this.formatter.formatPhoneNumber(call.calledTo),
          callLength: this.formatter.formatDuration(call.durationSec),
          callCharge: call.chargeYen,
        })
      ),
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
      month: targetMonth,
    };
  }

  /**
   * Get international call history for a user's SIM
   */
  async getInternationalCallHistory(
    options: CallHistoryQueryOptions
  ): Promise<SimInternationalCallHistoryResponse> {
    const { userId, subscriptionId, month, page = 1, limit = 50 } = options;
    await this.simValidation.validateSimSubscription(userId, subscriptionId);
    const account = "08077052946";

    const targetMonth = month || this.formatter.getDefaultMonth();

    const [calls, total] = await Promise.all([
      this.prisma.simCallHistoryInternational.findMany({
        where: { account, month: targetMonth },
        orderBy: [{ callDate: "desc" }, { startTime: "desc" }],
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.simCallHistoryInternational.count({
        where: { account, month: targetMonth },
      }),
    ]);

    return {
      calls: calls.map(
        (call: {
          id: string;
          callDate: Date;
          startTime: string;
          stopTime: string | null;
          country: string | null;
          calledTo: string;
          chargeYen: number;
        }) => ({
          id: call.id,
          date: call.callDate.toISOString().split("T")[0] ?? "",
          startTime: this.formatter.formatTime(call.startTime),
          stopTime: call.stopTime ? this.formatter.formatTime(call.stopTime) : null,
          country: call.country,
          calledTo: this.formatter.formatPhoneNumber(call.calledTo),
          callCharge: call.chargeYen,
        })
      ),
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
      month: targetMonth,
    };
  }

  /**
   * Get SMS history for a user's SIM
   */
  async getSmsHistory(options: CallHistoryQueryOptions): Promise<SimSmsHistoryResponse> {
    const { userId, subscriptionId, month, page = 1, limit = 50 } = options;
    await this.simValidation.validateSimSubscription(userId, subscriptionId);
    const account = "08077052946";

    const targetMonth = month || this.formatter.getDefaultMonth();

    const [messages, total] = await Promise.all([
      this.prisma.simSmsHistory.findMany({
        where: { account, month: targetMonth },
        orderBy: [{ smsDate: "desc" }, { smsTime: "desc" }],
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.simSmsHistory.count({
        where: { account, month: targetMonth },
      }),
    ]);

    return {
      messages: messages.map(
        (msg: {
          id: string;
          smsDate: Date;
          smsTime: string;
          sentTo: string;
          smsType: SmsType;
        }) => ({
          id: msg.id,
          date: msg.smsDate.toISOString().split("T")[0] ?? "",
          time: this.formatter.formatTime(msg.smsTime),
          sentTo: this.formatter.formatPhoneNumber(msg.sentTo),
          type: msg.smsType === "INTERNATIONAL" ? "International SMS" : "SMS",
        })
      ),
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
      month: targetMonth,
    };
  }

  /**
   * Get available months for history
   */
  async getAvailableMonths(): Promise<string[]> {
    this.logger.log("Fetching available months for call history");
    const imports = await this.prisma.simHistoryImport.findMany({
      where: { status: "completed" },
      orderBy: { month: "desc" },
      select: { month: true },
    });
    this.logger.log(`Found ${imports.length} completed imports`, { months: imports });
    return imports.map((i: { month: string }) => i.month);
  }

  /**
   * List available files on SFTP server
   */
  async listSftpFiles(path: string = "/"): Promise<string[]> {
    try {
      return await this.sftp.listFiles(path);
    } catch (error) {
      this.logger.error("Failed to list SFTP files", { error, path });
      throw error;
    }
  }

  /**
   * Process records in batches with error handling
   */
  private async processInBatches<T>(
    records: T[],
    batchSize: number,
    handler: (record: T) => Promise<unknown>,
    onError: (record: T, error: unknown) => void
  ): Promise<number> {
    const batches = this.createBatches(records, batchSize);
    const batchResults = await Promise.all(
      batches.map(async batch => this.processSingleBatch(batch, handler, onError))
    );
    return batchResults.reduce((sum, count) => sum + count, 0);
  }

  private createBatches<T>(records: T[], batchSize: number): T[][] {
    const batches: T[][] = [];
    for (let i = 0; i < records.length; i += batchSize) {
      batches.push(records.slice(i, i + batchSize));
    }
    return batches;
  }

  private async processSingleBatch<T>(
    batch: T[],
    handler: (record: T) => Promise<unknown>,
    onError: (record: T, error: unknown) => void
  ): Promise<number> {
    const results = await Promise.allSettled(batch.map(async record => handler(record)));
    let successCount = 0;

    for (const [index, result] of results.entries()) {
      if (result.status === "fulfilled") {
        successCount++;
      } else {
        const record = batch[index];
        if (record !== undefined) {
          onError(record, result.reason);
        }
      }
    }

    return successCount;
  }
}
