import http from "node:http";
import https from "node:https";
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import path from "node:path";
import type {
  SkillDownloadState,
  SkillSummary,
  WorkspaceSkillSummary
} from "@qjclaw/shared-types";

export interface RemoteSkillAsset {
  bindingId: string;
  skillId: string;
  name: string;
  description: string;
  category: string;
  fileName?: string;
  fileSize?: number;
  downloadUrl?: string;
}

export interface SkillExecutionTarget {
  skillId: string;
  name: string;
  fileName?: string;
  localPath: string;
}

interface SkillManifestEntry extends RemoteSkillAsset {
  localPath?: string;
  downloadState: SkillDownloadState;
  lastSyncedAt?: string;
  lastDownloadedAt?: string;
  lastError?: string;
  remoteConfigVersion?: string;
}

const SKILLS_DIR = "skills";
const MANIFEST_FILE = "manifest.json";
const REDIRECT_STATUS_CODES = new Set([301, 302, 307, 308]);
const MAX_REDIRECTS = 5;

function toSummary(entry: SkillManifestEntry): WorkspaceSkillSummary {
  return {
    id: entry.skillId,
    name: entry.name,
    description: entry.description,
    category: entry.category,
    enabled: entry.downloadState !== "removed",
    ready: entry.downloadState === "ready",
    downloadState: entry.downloadState,
    fileName: entry.fileName,
    fileSize: entry.fileSize,
    lastSyncedAt: entry.lastSyncedAt,
    lastError: entry.lastError
  };
}

function compareEntries(left: SkillManifestEntry, right: SkillManifestEntry): number {
  return left.name.localeCompare(right.name, "zh-CN");
}

export class SkillStoreService {
  private readonly skillsRoot: string;
  private readonly manifestPath: string;

  constructor(userDataPath: string) {
    this.skillsRoot = path.join(userDataPath, SKILLS_DIR);
    this.manifestPath = path.join(this.skillsRoot, MANIFEST_FILE);
  }

  async reconcile(remoteSkills: RemoteSkillAsset[], configVersion?: string): Promise<void> {
    await mkdir(this.skillsRoot, { recursive: true });

    const now = new Date().toISOString();
    const currentEntries = await this.readManifest();
    const currentById = new Map(currentEntries.map((entry) => [entry.skillId, entry]));
    const nextEntries: SkillManifestEntry[] = [];

    for (const remoteSkill of remoteSkills) {
      const current = currentById.get(remoteSkill.skillId);
      const nextEntry: SkillManifestEntry = {
        ...current,
        ...remoteSkill,
        localPath: remoteSkill.fileName
          ? path.join(this.skillsRoot, remoteSkill.skillId, remoteSkill.fileName)
          : current?.localPath,
        downloadState: current?.downloadState ?? "pending",
        lastSyncedAt: now,
        remoteConfigVersion: configVersion ?? current?.remoteConfigVersion
      };

      if (!remoteSkill.downloadUrl || !remoteSkill.fileName) {
        nextEntries.push({
          ...nextEntry,
          downloadState: "failed",
          lastError: "技能下载地址或文件名缺失。"
        });
        continue;
      }

      if (!this.needsDownload(current, nextEntry)) {
        nextEntries.push({
          ...nextEntry,
          downloadState: "ready",
          lastError: undefined
        });
        continue;
      }

      try {
        const downloadingEntry: SkillManifestEntry = {
          ...nextEntry,
          downloadState: "downloading",
          lastError: undefined
        };
        const downloadedEntry = await this.downloadSkill(downloadingEntry);
        nextEntries.push(downloadedEntry);
      } catch (error) {
        nextEntries.push({
          ...nextEntry,
          downloadState: "failed",
          lastError: error instanceof Error ? error.message : String(error)
        });
      }
    }

    const remoteSkillIds = new Set(remoteSkills.map((skill) => skill.skillId));
    for (const current of currentEntries) {
      if (!remoteSkillIds.has(current.skillId)) {
        nextEntries.push({
          ...current,
          downloadState: "removed",
          lastSyncedAt: now,
          remoteConfigVersion: configVersion ?? current.remoteConfigVersion
        });
      }
    }

    await this.writeManifest(nextEntries);
  }

  async listWorkspaceSkills(): Promise<WorkspaceSkillSummary[]> {
    const entries = await this.readManifest();
    return entries
      .filter((entry) => entry.downloadState !== "removed")
      .sort(compareEntries)
      .map(toSummary);
  }

  async listSkills(): Promise<SkillSummary[]> {
    const entries = await this.readManifest();
    return entries
      .filter((entry) => entry.downloadState !== "removed")
      .sort(compareEntries)
      .map((entry) => ({
        ...toSummary(entry),
        requiresCredits: 0
      }));
  }

  async getExecutionTarget(skillId: string): Promise<SkillExecutionTarget | undefined> {
    const entries = await this.readManifest();
    const entry = entries.find((item) => item.skillId === skillId && item.downloadState === "ready" && typeof item.localPath === "string");
    if (!entry?.localPath) {
      return undefined;
    }

    return {
      skillId: entry.skillId,
      name: entry.name,
      fileName: entry.fileName,
      localPath: entry.localPath
    };
  }

  private needsDownload(current: SkillManifestEntry | undefined, next: SkillManifestEntry): boolean {
    if (!current) {
      return true;
    }

    if (current.downloadState !== "ready") {
      return true;
    }

    if (!current.localPath) {
      return true;
    }

    return current.downloadUrl !== next.downloadUrl
      || current.fileName !== next.fileName
      || current.fileSize !== next.fileSize;
  }

  private async downloadSkill(entry: SkillManifestEntry): Promise<SkillManifestEntry> {
    if (!entry.downloadUrl || !entry.fileName || !entry.localPath) {
      throw new Error("技能下载元数据不完整，无法落盘。");
    }

    const targetDir = path.dirname(entry.localPath);
    const tempPath = `${entry.localPath}.tmp-${Date.now()}`;
    const downloadedAt = new Date().toISOString();

    await mkdir(targetDir, { recursive: true });
    try {
      const payload = await this.downloadToBuffer(new URL(entry.downloadUrl));
      await writeFile(tempPath, payload);
      await unlink(entry.localPath).catch(() => undefined);
      await rename(tempPath, entry.localPath);
    } catch (error) {
      await unlink(tempPath).catch(() => undefined);
      throw error;
    }

    return {
      ...entry,
      downloadState: "ready",
      lastDownloadedAt: downloadedAt,
      lastError: undefined
    };
  }

  private async downloadToBuffer(url: URL, redirectCount = 0): Promise<Buffer> {
    const client = url.protocol === "https:" ? https : http;

    return new Promise<Buffer>((resolve, reject) => {
      const request = client.get(url, (response) => {
        const status = response.statusCode ?? 500;
        const location = response.headers.location;

        if (location && REDIRECT_STATUS_CODES.has(status)) {
          if (redirectCount >= MAX_REDIRECTS) {
            reject(new Error("技能下载重定向次数过多。"));
            response.resume();
            return;
          }

          const redirectUrl = new URL(location, url);
          response.resume();
          void this.downloadToBuffer(redirectUrl, redirectCount + 1).then(resolve, reject);
          return;
        }

        if (status < 200 || status >= 300) {
          reject(new Error(`技能下载失败，HTTP 状态码 ${status}。`));
          response.resume();
          return;
        }

        const chunks: Buffer[] = [];
        response.on("data", (chunk) => {
          chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
        });
        response.on("end", () => {
          resolve(Buffer.concat(chunks));
        });
      });

      request.on("error", (error) => {
        reject(new Error(`技能下载失败：${error.message}`));
      });
    });
  }

  private async readManifest(): Promise<SkillManifestEntry[]> {
    await mkdir(this.skillsRoot, { recursive: true });

    try {
      const raw = await readFile(this.manifestPath, "utf8");
      const parsed = JSON.parse(raw);
      if (!Array.isArray(parsed)) {
        return [];
      }

      return parsed.filter((entry): entry is SkillManifestEntry => typeof entry?.skillId === "string");
    } catch {
      return [];
    }
  }

  private async writeManifest(entries: SkillManifestEntry[]): Promise<void> {
    const nextEntries = [...entries].sort(compareEntries);
    const tempPath = `${this.manifestPath}.tmp-${Date.now()}`;

    await mkdir(this.skillsRoot, { recursive: true });
    await writeFile(tempPath, JSON.stringify(nextEntries, null, 2), "utf8");
    await unlink(this.manifestPath).catch(() => undefined);
    await rename(tempPath, this.manifestPath);
  }
}
