import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import path from "node:path";
import type { DailyReportDeliveryState, OpenClawDailyReportPayload } from "@qjclaw/shared-types";
import { OpenClawDailyReportClient } from "./cloud-api.js";
import type { RuntimeCloudActivityEvent } from "./runtime-cloud-supervisor.js";
import { getRuntimeCloudApiTarget, type AppConfigService } from "./app-config.js";
import type { SecretManager } from "./secrets.js";

interface PersistedDailyReportEntry {
  summaryDate: string;
  conversationIds: string[];
  messageReceivedCount: number;
  messageSentCount: number;
  errorCount: number;
  tokenDeltaTotal: number;
  issueSamples: string[];
  deliveryState: DailyReportDeliveryState;
  lastAttemptAt?: string;
  lastSentAt?: string;
  lastError?: string;
  updatedAt: string;
}

interface DailyReportPersistenceState {
  reports: Record<string, PersistedDailyReportEntry>;
}

interface DailyReportServiceOptions {
  userDataPath: string;
  configService: AppConfigService;
  secretManager: SecretManager;
}

interface ReportTime {
  hour: number;
  minute: number;
}

const DAILY_REPORT_DIR = "daily-reports";
const DAILY_REPORT_STATE_FILE = "state.json";
const DEFAULT_REPORT_TIME = "00:05";
const CHECK_INTERVAL_MS = 60 * 1000;
const MAX_ISSUES = 5;
const MAX_ISSUE_LENGTH = 240;

function formatLocalDate(date: Date): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function getPreviousLocalDate(date: Date): string {
  const previous = new Date(date);
  previous.setDate(previous.getDate() - 1);
  return formatLocalDate(previous);
}

function parseReportTime(raw?: string): ReportTime {
  const trimmed = raw?.trim() || DEFAULT_REPORT_TIME;
  const match = trimmed.match(/^(\d{1,2}):(\d{2})$/);
  if (!match) {
    return parseReportTime(DEFAULT_REPORT_TIME);
  }

  const hour = Number.parseInt(match[1], 10);
  const minute = Number.parseInt(match[2], 10);
  if (!Number.isInteger(hour) || !Number.isInteger(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
    return parseReportTime(DEFAULT_REPORT_TIME);
  }

  return { hour, minute };
}

function hasReachedScheduledTime(date: Date, reportTime: ReportTime): boolean {
  return date.getHours() > reportTime.hour
    || (date.getHours() === reportTime.hour && date.getMinutes() >= reportTime.minute);
}

function sanitizeIssue(errorType: string, message: string): string {
  const compact = message.replace(/\s+/g, " ").trim();
  const merged = `${errorType}: ${compact || "unknown error"}`;
  return merged.length > MAX_ISSUE_LENGTH ? `${merged.slice(0, MAX_ISSUE_LENGTH - 3)}...` : merged;
}

function createEmptyEntry(summaryDate: string): PersistedDailyReportEntry {
  return {
    summaryDate,
    conversationIds: [],
    messageReceivedCount: 0,
    messageSentCount: 0,
    errorCount: 0,
    tokenDeltaTotal: 0,
    issueSamples: [],
    deliveryState: "draft",
    updatedAt: new Date().toISOString()
  };
}

function normalizeEntry(summaryDate: string, value: unknown): PersistedDailyReportEntry {
  const record = typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
  return {
    summaryDate,
    conversationIds: Array.isArray(record.conversationIds)
      ? record.conversationIds.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
      : [],
    messageReceivedCount: typeof record.messageReceivedCount === "number" ? record.messageReceivedCount : 0,
    messageSentCount: typeof record.messageSentCount === "number" ? record.messageSentCount : 0,
    errorCount: typeof record.errorCount === "number" ? record.errorCount : 0,
    tokenDeltaTotal: typeof record.tokenDeltaTotal === "number" ? record.tokenDeltaTotal : 0,
    issueSamples: Array.isArray(record.issueSamples)
      ? record.issueSamples.filter((item): item is string => typeof item === "string" && item.trim().length > 0).slice(0, MAX_ISSUES)
      : [],
    deliveryState: record.deliveryState === "sent" || record.deliveryState === "failed" ? record.deliveryState : "draft",
    lastAttemptAt: typeof record.lastAttemptAt === "string" ? record.lastAttemptAt : undefined,
    lastSentAt: typeof record.lastSentAt === "string" ? record.lastSentAt : undefined,
    lastError: typeof record.lastError === "string" ? record.lastError : undefined,
    updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : new Date().toISOString()
  };
}

function normalizeState(value: unknown): DailyReportPersistenceState {
  const record = typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
  const reportsRecord = typeof record.reports === "object" && record.reports !== null ? record.reports as Record<string, unknown> : {};
  const reports: Record<string, PersistedDailyReportEntry> = {};

  for (const [summaryDate, entry] of Object.entries(reportsRecord)) {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(summaryDate)) {
      continue;
    }
    reports[summaryDate] = normalizeEntry(summaryDate, entry);
  }

  return { reports };
}

export class DailyReportService {
  private readonly configService: AppConfigService;
  private readonly secretManager: SecretManager;
  private readonly reportClient: OpenClawDailyReportClient;
  private readonly reportsRoot: string;
  private readonly statePath: string;
  private readonly reportTime = parseReportTime(process.env.QJCLAW_DAILY_REPORT_TIME);
  private state: DailyReportPersistenceState = { reports: {} };
  private loadPromise?: Promise<void>;
  private writeChain: Promise<void> = Promise.resolve();
  private timer?: NodeJS.Timeout;

  constructor(options: DailyReportServiceOptions) {
    this.configService = options.configService;
    this.secretManager = options.secretManager;
    this.reportClient = new OpenClawDailyReportClient(options.configService, options.secretManager);
    this.reportsRoot = path.join(options.userDataPath, DAILY_REPORT_DIR);
    this.statePath = path.join(this.reportsRoot, DAILY_REPORT_STATE_FILE);
  }

  async start(): Promise<void> {
    await this.ensureLoaded();
    if (!this.timer) {
      this.timer = setInterval(() => {
        void this.runDueCheck().catch((error) => {
          this.logError("Daily report scheduled check failed", error);
        });
      }, CHECK_INTERVAL_MS);
    }

    void this.runDueCheck().catch((error) => {
      this.logError("Daily report startup check failed", error);
    });
  }

  async stop(): Promise<void> {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = undefined;
    }

    await this.writeChain;
  }

  async runDueCheck(now = new Date()): Promise<void> {
    await this.ensureLoaded();
    if (!hasReachedScheduledTime(now, this.reportTime)) {
      return;
    }

    if (!await this.canSend()) {
      return;
    }

    const summaryDate = getPreviousLocalDate(now);
    const entry = this.state.reports[summaryDate];
    if (!entry || entry.deliveryState === "sent") {
      return;
    }

    await this.submitEntry(entry);
  }

  handleActivity(event: RuntimeCloudActivityEvent): void {
    if (!this.loadPromise) {
      return;
    }

    switch (event.type) {
      case "message_received":
        this.recordMessage(event.sessionId, event.occurredAt, "received");
        return;
      case "message_sent":
        this.recordMessage(event.sessionId, event.occurredAt, "sent");
        return;
      case "error":
        this.recordError(event.errorType, event.message, event.occurredAt, event.sessionId);
        return;
      case "heartbeat":
        this.recordHeartbeat(event.occurredAt, event.billing?.tokenDelta);
        return;
      default:
        return;
    }
  }

  private async ensureLoaded(): Promise<void> {
    if (!this.loadPromise) {
      this.loadPromise = this.loadState();
    }

    await this.loadPromise;
  }

  private async loadState(): Promise<void> {
    await mkdir(this.reportsRoot, { recursive: true });

    try {
      const raw = await readFile(this.statePath, "utf8");
      this.state = normalizeState(JSON.parse(raw));
    } catch {
      this.state = { reports: {} };
      await this.persistState();
    }
  }

  private persistStateSoon(): void {
    void this.persistState().catch((error) => {
      this.logError("Daily report persist failed", error);
    });
  }

  private recordMessage(sessionId: string, occurredAt: string, kind: "received" | "sent"): void {
    const entry = this.getOrCreateEntry(formatLocalDate(new Date(occurredAt)));
    if (sessionId && !entry.conversationIds.includes(sessionId)) {
      entry.conversationIds.push(sessionId);
    }
    if (kind === "received") {
      entry.messageReceivedCount += 1;
    } else {
      entry.messageSentCount += 1;
    }
    entry.updatedAt = new Date().toISOString();
    this.persistStateSoon();
  }

  private recordError(errorType: string, message: string, occurredAt: string, sessionId?: string): void {
    const entry = this.getOrCreateEntry(formatLocalDate(new Date(occurredAt)));
    if (sessionId && !entry.conversationIds.includes(sessionId)) {
      entry.conversationIds.push(sessionId);
    }
    entry.errorCount += 1;
    const issue = sanitizeIssue(errorType, message);
    if (!entry.issueSamples.includes(issue)) {
      entry.issueSamples.push(issue);
      if (entry.issueSamples.length > MAX_ISSUES) {
        entry.issueSamples = entry.issueSamples.slice(-MAX_ISSUES);
      }
    }
    entry.updatedAt = new Date().toISOString();
    this.persistStateSoon();
  }

  private recordHeartbeat(occurredAt: string, tokenDelta?: number): void {
    if (!Number.isFinite(tokenDelta)) {
      return;
    }

    const entry = this.getOrCreateEntry(formatLocalDate(new Date(occurredAt)));
    entry.tokenDeltaTotal += Number(tokenDelta ?? 0);
    entry.updatedAt = new Date().toISOString();
    this.persistStateSoon();
  }

  private getOrCreateEntry(summaryDate: string): PersistedDailyReportEntry {
    const existing = this.state.reports[summaryDate];
    if (existing) {
      return existing;
    }

    const created = createEmptyEntry(summaryDate);
    this.state.reports[summaryDate] = created;
    return created;
  }

  private async submitEntry(entry: PersistedDailyReportEntry): Promise<void> {
    const attemptedAt = new Date().toISOString();
    entry.lastAttemptAt = attemptedAt;
    entry.updatedAt = attemptedAt;
    await this.persistState();

    try {
      const payload = this.buildPayload(entry);
      await this.reportClient.submit(payload);
      entry.deliveryState = "sent";
      entry.lastSentAt = new Date().toISOString();
      entry.lastError = undefined;
    } catch (error) {
      entry.deliveryState = "failed";
      entry.lastError = error instanceof Error ? error.message : String(error);
      this.logError(`Daily report submit failed for ${entry.summaryDate}`, error);
    }

    entry.updatedAt = new Date().toISOString();
    await this.persistState();
  }

  private buildPayload(entry: PersistedDailyReportEntry): Omit<OpenClawDailyReportPayload, "api_key"> {
    const totalConversations = entry.conversationIds.length;
    const totalMessages = entry.messageReceivedCount + entry.messageSentCount;
    const totalTokens = Math.max(0, Math.round(entry.tokenDeltaTotal));
    const totalErrors = entry.errorCount;
    const issues = entry.issueSamples.slice(0, MAX_ISSUES);

    return {
      summary_date: entry.summaryDate,
      total_conversations: totalConversations,
      total_messages: totalMessages,
      total_tokens: totalTokens,
      total_errors: totalErrors,
      highlights: this.buildHighlights(totalConversations, totalMessages, totalTokens, totalErrors),
      issues,
      raw_summary_text: this.buildSummaryText(totalConversations, totalMessages, totalTokens, totalErrors),
      active_channels: []
    };
  }

  private buildHighlights(totalConversations: number, totalMessages: number, totalTokens: number, totalErrors: number): string[] {
    const highlights = [`处理 ${totalConversations} 个会话，共 ${totalMessages} 条消息。`];
    if (totalTokens > 0) {
      highlights.push(`累计上报 ${totalTokens} 个 token。`);
    }
    highlights.push(totalErrors > 0 ? `记录到 ${totalErrors} 次异常。` : "未记录异常。");
    return highlights;
  }

  private buildSummaryText(totalConversations: number, totalMessages: number, totalTokens: number, totalErrors: number): string {
    const segments = [`当日共处理 ${totalConversations} 个会话`, `${totalMessages} 条消息`];
    if (totalTokens > 0) {
      segments.push(`累计 ${totalTokens} 个 token`);
    }
    segments.push(totalErrors > 0 ? `出现 ${totalErrors} 次异常` : "未记录异常");
    return `${segments.join("，")}。`;
  }

  private async canSend(): Promise<boolean> {
    const config = await this.configService.load();
    const apiKey = (await this.secretManager.getApiKey())?.trim();
    return Boolean(getRuntimeCloudApiTarget(config).baseUrl && apiKey);
  }

  private async persistState(): Promise<void> {
    const snapshot = JSON.stringify(this.state, null, 2);
    const tempPath = `${this.statePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;

    this.writeChain = this.writeChain.catch(() => undefined).then(async () => {
      await mkdir(this.reportsRoot, { recursive: true });
      await writeFile(tempPath, snapshot, "utf8");
      await unlink(this.statePath).catch(() => undefined);
      await rename(tempPath, this.statePath);
    });

    await this.writeChain;
  }

  private logError(prefix: string, error: unknown): void {
    const message = error instanceof Error ? error.message : String(error);
    console.error(`${prefix}: ${message}`);
  }
}




