import { randomUUID } from "node:crypto";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path";
import type {
  AutomationTask,
  AutomationTaskRun,
  AutomationTaskSchedule,
  CreateAutomationTaskInput,
  TaskPanelArtifact,
  UpdateAutomationTaskInput
} from "@qjclaw/shared-types";

interface AutomationTaskState {
  tasks: AutomationTask[];
  runs: AutomationTaskRun[];
}

export interface AutomationTaskExecutorInput {
  task: AutomationTask;
  run: AutomationTaskRun;
}

export interface AutomationTaskExecutorResult {
  sessionId?: string;
  runId?: string;
  replyText?: string;
  artifacts?: TaskPanelArtifact[];
}

export type AutomationTaskExecutor = (input: AutomationTaskExecutorInput) => Promise<AutomationTaskExecutorResult>;

export interface AutomationTaskServiceOptions {
  now?: () => Date;
  autoStart?: boolean;
}

const MAX_TIMER_DELAY_MS = 60_000;
const VALID_TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/;

function createEmptyState(): AutomationTaskState {
  return {
    tasks: [],
    runs: []
  };
}

function isObject(value: unknown): value is Record<string, unknown> {
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
}

function isAutomationTaskState(value: unknown): value is AutomationTaskState {
  return Boolean(
    isObject(value)
      && Array.isArray(value.tasks)
      && Array.isArray(value.runs)
  );
}

function parseValidDate(value?: string): Date | null {
  if (!value) {
    return null;
  }
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? null : date;
}

function parseTime(value: string): { hour: number; minute: number } | null {
  const match = value.match(VALID_TIME_PATTERN);
  if (!match) {
    return null;
  }
  return {
    hour: Number(match[1]),
    minute: Number(match[2])
  };
}

function localCandidate(from: Date, offsetDays: number, time: { hour: number; minute: number }): Date {
  return new Date(
    from.getFullYear(),
    from.getMonth(),
    from.getDate() + offsetDays,
    time.hour,
    time.minute,
    0,
    0
  );
}

export function computeAutomationNextRunAt(schedule: AutomationTaskSchedule, from = new Date()): string | undefined {
  if (schedule.kind === "once") {
    const runAt = parseValidDate(schedule.runAt);
    return runAt && runAt.getTime() > from.getTime() ? runAt.toISOString() : undefined;
  }

  const time = parseTime(schedule.time);
  if (!time) {
    return undefined;
  }

  if (schedule.kind === "daily") {
    const today = localCandidate(from, 0, time);
    return (today.getTime() > from.getTime() ? today : localCandidate(from, 1, time)).toISOString();
  }

  const weekdays = [...new Set(schedule.weekdays)]
    .filter((day) => Number.isInteger(day) && day >= 0 && day <= 6)
    .sort((left, right) => left - right);
  if (!weekdays.length) {
    return undefined;
  }

  for (let offset = 0; offset <= 7; offset += 1) {
    const candidate = localCandidate(from, offset, time);
    if (weekdays.includes(candidate.getDay()) && candidate.getTime() > from.getTime()) {
      return candidate.toISOString();
    }
  }

  return undefined;
}

function normalizeTitle(value: string): string {
  const trimmed = value.trim();
  return trimmed || "未命名自动化任务";
}

function normalizePrompt(value: string): string {
  const trimmed = value.trim();
  if (!trimmed) {
    throw new Error("Automation task prompt is required.");
  }
  return trimmed;
}

function normalizeSchedule(schedule: AutomationTaskSchedule): AutomationTaskSchedule {
  if (schedule.kind === "once") {
    const runAt = parseValidDate(schedule.runAt);
    if (!runAt) {
      throw new Error("One-time automation task runAt is invalid.");
    }
    return {
      kind: "once",
      runAt: runAt.toISOString()
    };
  }

  if (!parseTime(schedule.time)) {
    throw new Error("Automation task time must use HH:mm.");
  }

  if (schedule.kind === "daily") {
    return {
      kind: "daily",
      time: schedule.time
    };
  }

  const weekdays = [...new Set(schedule.weekdays)]
    .filter((day) => Number.isInteger(day) && day >= 0 && day <= 6)
    .sort((left, right) => left - right);
  if (!weekdays.length) {
    throw new Error("Weekly automation task requires at least one weekday.");
  }
  return {
    kind: "weekly",
    time: schedule.time,
    weekdays
  };
}

function assertEnabledScheduleCanRun(schedule: AutomationTaskSchedule, enabled: boolean, from: Date): void {
  if (enabled && schedule.kind === "once" && !computeAutomationNextRunAt(schedule, from)) {
    throw new Error("Enabled one-time automation task runAt must be in the future.");
  }
}

export class AutomationTaskService {
  private readonly statePath: string;
  private readonly now: () => Date;
  private executor: AutomationTaskExecutor | null = null;
  private timer: NodeJS.Timeout | null = null;
  private writeChain: Promise<unknown> = Promise.resolve();

  constructor(userDataPath: string, options: AutomationTaskServiceOptions = {}) {
    this.statePath = path.join(userDataPath, "automation-tasks", "state.json");
    this.now = options.now ?? (() => new Date());
    if (options.autoStart !== false) {
      void this.start();
    }
  }

  setExecutor(executor: AutomationTaskExecutor): void {
    this.executor = executor;
  }

  async start(): Promise<void> {
    await this.recoverMissedRuns();
    this.scheduleNextTimer();
  }

  stop(): void {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
  }

  async list(): Promise<AutomationTask[]> {
    const state = await this.loadState();
    return [...state.tasks].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
  }

  async listRuns(taskId?: string): Promise<AutomationTaskRun[]> {
    const normalizedTaskId = taskId?.trim();
    const state = await this.loadState();
    return state.runs
      .filter((run) => !normalizedTaskId || run.taskId === normalizedTaskId)
      .sort((left, right) => (right.startedAt ?? right.completedAt ?? "").localeCompare(left.startedAt ?? left.completedAt ?? ""));
  }

  async create(input: CreateAutomationTaskInput): Promise<AutomationTask> {
    return this.enqueueWrite(async () => {
      const state = await this.loadState();
      const now = this.now();
      const nowIso = now.toISOString();
      const schedule = normalizeSchedule(input.schedule);
      const enabled = Boolean(input.enabled);
      assertEnabledScheduleCanRun(schedule, enabled, now);
      const task: AutomationTask = {
        id: randomUUID(),
        title: normalizeTitle(input.title),
        prompt: normalizePrompt(input.prompt),
        expertId: input.expertId?.trim() || undefined,
        expertName: input.expertName?.trim() || undefined,
        enabled,
        schedule,
        nextRunAt: enabled ? computeAutomationNextRunAt(schedule, now) : undefined,
        createdAt: nowIso,
        updatedAt: nowIso
      };
      state.tasks.push(task);
      await this.saveState(state);
      this.scheduleNextTimerFromState(state);
      return task;
    });
  }

  async update(taskId: string, input: UpdateAutomationTaskInput): Promise<AutomationTask | null> {
    return this.enqueueWrite(async () => {
      const state = await this.loadState();
      const index = state.tasks.findIndex((task) => task.id === taskId);
      if (index < 0) {
        return null;
      }

      const current = state.tasks[index]!;
      const schedule = input.schedule ? normalizeSchedule(input.schedule) : current.schedule;
      const enabled = input.enabled ?? current.enabled;
      const now = this.now();
      assertEnabledScheduleCanRun(schedule, enabled, now);
      const next: AutomationTask = {
        ...current,
        title: input.title !== undefined ? normalizeTitle(input.title) : current.title,
        prompt: input.prompt !== undefined ? normalizePrompt(input.prompt) : current.prompt,
        expertId: input.expertId === null ? undefined : input.expertId?.trim() || current.expertId,
        expertName: input.expertName === null ? undefined : input.expertName?.trim() || current.expertName,
        enabled,
        schedule,
        nextRunAt: enabled ? computeAutomationNextRunAt(schedule, now) : undefined,
        updatedAt: now.toISOString()
      };
      state.tasks[index] = next;
      await this.saveState(state);
      this.scheduleNextTimerFromState(state);
      return next;
    });
  }

  async delete(taskId: string): Promise<boolean> {
    return this.enqueueWrite(async () => {
      const state = await this.loadState();
      const nextTasks = state.tasks.filter((task) => task.id !== taskId);
      const deleted = nextTasks.length !== state.tasks.length;
      if (!deleted) {
        return false;
      }
      state.tasks = nextTasks;
      state.runs = state.runs.filter((run) => run.taskId !== taskId);
      await this.saveState(state);
      this.scheduleNextTimerFromState(state);
      return true;
    });
  }

  async runNow(taskId: string): Promise<AutomationTaskRun> {
    const state = await this.loadState();
    const task = state.tasks.find((candidate) => candidate.id === taskId);
    if (!task) {
      throw new Error("Automation task not found.");
    }
    return this.executeTask(task, "manual");
  }

  async recoverMissedRuns(): Promise<void> {
    await this.enqueueWrite(async () => {
      const state = await this.loadState();
      const now = this.now();
      let changed = false;

      state.tasks = state.tasks.map((task) => {
        if (!task.enabled || !task.nextRunAt) {
          return task;
        }
        const nextRun = parseValidDate(task.nextRunAt);
        if (!nextRun || nextRun.getTime() > now.getTime()) {
          return task;
        }

        const completedAt = now.toISOString();
        state.runs.push({
          id: randomUUID(),
          taskId: task.id,
          trigger: "startup",
          status: "missed",
          scheduledFor: task.nextRunAt,
          startedAt: completedAt,
          completedAt,
          error: "应用未运行，已跳过本次计划执行。",
          artifacts: []
        });
        changed = true;

        const nextRunAt = task.schedule.kind === "once"
          ? undefined
          : computeAutomationNextRunAt(task.schedule, now);
        return {
          ...task,
          enabled: task.schedule.kind === "once" ? false : task.enabled,
          nextRunAt,
          lastRunAt: completedAt,
          lastRunStatus: "missed" as const,
          lastRunError: "应用未运行，已跳过本次计划执行。",
          updatedAt: completedAt
        };
      });

      if (changed) {
        await this.saveState(state);
      }
      this.scheduleNextTimerFromState(state);
    });
  }

  private async runDueTasks(): Promise<void> {
    const state = await this.loadState();
    const now = this.now();
    const dueTasks = state.tasks.filter((task) => {
      const nextRun = parseValidDate(task.nextRunAt);
      return task.enabled && nextRun && nextRun.getTime() <= now.getTime();
    });

    for (const task of dueTasks) {
      await this.executeTask(task, "schedule", task.nextRunAt);
    }

    this.scheduleNextTimer();
  }

  private async executeTask(
    task: AutomationTask,
    trigger: AutomationTaskRun["trigger"],
    scheduledFor?: string
  ): Promise<AutomationTaskRun> {
    const executor = this.executor;
    if (!executor) {
      throw new Error("Automation task executor is not configured.");
    }

    const startedAt = this.now().toISOString();
    const run: AutomationTaskRun = {
      id: randomUUID(),
      taskId: task.id,
      trigger,
      status: "running",
      scheduledFor,
      startedAt,
      artifacts: []
    };

    await this.enqueueWrite(async () => {
      const state = await this.loadState();
      const taskIndex = state.tasks.findIndex((candidate) => candidate.id === task.id);
      if (taskIndex >= 0 && trigger === "schedule") {
        const current = state.tasks[taskIndex]!;
        const nextRunAt = current.schedule.kind === "once"
          ? undefined
          : computeAutomationNextRunAt(current.schedule, parseValidDate(scheduledFor) ?? this.now());
        state.tasks[taskIndex] = {
          ...current,
          enabled: current.schedule.kind === "once" ? false : current.enabled,
          nextRunAt,
          updatedAt: startedAt
        };
      }
      state.runs.push(run);
      await this.saveState(state);
    });

    try {
      const result = await executor({ task, run });
      const completed: AutomationTaskRun = {
        ...run,
        status: "completed",
        completedAt: this.now().toISOString(),
        sessionId: result.sessionId,
        runId: result.runId,
        replyText: result.replyText,
        artifacts: result.artifacts ?? []
      };
      await this.finishRun(completed);
      return completed;
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      const failed: AutomationTaskRun = {
        ...run,
        status: "failed",
        completedAt: this.now().toISOString(),
        error: message,
        artifacts: []
      };
      await this.finishRun(failed);
      return failed;
    }
  }

  private async finishRun(run: AutomationTaskRun): Promise<void> {
    await this.enqueueWrite(async () => {
      const state = await this.loadState();
      state.runs = state.runs.map((candidate) => candidate.id === run.id ? run : candidate);
      state.tasks = state.tasks.map((task) => task.id === run.taskId
        ? {
            ...task,
            lastRunAt: run.completedAt ?? run.startedAt,
            lastRunStatus: run.status,
            lastRunError: run.error,
            updatedAt: run.completedAt ?? this.now().toISOString()
          }
        : task
      );
      await this.saveState(state);
      this.scheduleNextTimerFromState(state);
    });
  }

  private async enqueueWrite<T>(operation: () => Promise<T>): Promise<T> {
    const next = this.writeChain
      .catch(() => undefined)
      .then(operation);
    this.writeChain = next;
    return next;
  }

  private scheduleNextTimer(): void {
    void this.loadState().then((state) => this.scheduleNextTimerFromState(state)).catch(() => undefined);
  }

  private scheduleNextTimerFromState(state: AutomationTaskState): void {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }

    const nowMs = this.now().getTime();
    const nextRunMs = state.tasks
      .filter((task) => task.enabled)
      .map((task) => parseValidDate(task.nextRunAt)?.getTime())
      .filter((value): value is number => typeof value === "number")
      .sort((left, right) => left - right)[0];
    if (nextRunMs === undefined) {
      return;
    }

    const delay = Math.max(0, Math.min(nextRunMs - nowMs, MAX_TIMER_DELAY_MS));
    this.timer = setTimeout(() => {
      void this.runDueTasks().catch(() => this.scheduleNextTimer());
    }, delay);
    this.timer.unref?.();
  }

  private async loadState(): Promise<AutomationTaskState> {
    try {
      const raw = await readFile(this.statePath, "utf8");
      const parsed = JSON.parse(raw) as unknown;
      return isAutomationTaskState(parsed) ? parsed : createEmptyState();
    } catch {
      return createEmptyState();
    }
  }

  private async saveState(state: AutomationTaskState): Promise<void> {
    await mkdir(path.dirname(this.statePath), { recursive: true });
    const tempPath = this.statePath + ".tmp";
    await writeFile(tempPath, JSON.stringify(state, null, 2), "utf8");
    await rename(tempPath, this.statePath);
  }
}
