import { Provide, Inject, Scope, ScopeEnum, Logger, Init } from '@midwayjs/core';
import { ILogger } from '@midwayjs/logger';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import * as fs from 'fs/promises';
import * as path from 'path';
import { execAsync } from '../../../comm/child_process.js';
import * as AdmZip from 'adm-zip';
import { NetaClawSkillEntity } from '../entity/skill.js';
import { resolveDataDir } from '../../../comm/data-dir.js';
import { SkillLoaderService, SkillMeta } from './skill_loader.js';
import { SkillRegistryService, SkillOrigin } from './skill_registry.js';

// ZIP 安全限制
const MAX_ZIP_SIZE = 50 * 1024 * 1024; // 50MB
const MAX_SINGLE_FILE = 1 * 1024 * 1024; // 1MB

// 安全校验正则（对齐 OpenClaw）
const SAFE_GITHUB_URL = /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+(\.git)?$/;
const SAFE_NODE_PACKAGE = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+(@[a-z0-9^~>=<.*|-]+)?$/;
const SAFE_UV_PACKAGE = /^[a-z0-9][a-z0-9._-]*(\[[a-z0-9,._-]+\])?(([><=!~]=?|===?)[a-z0-9.*_-]+)?$/i;

@Provide()
@Scope(ScopeEnum.Singleton)
export class SkillInstallerService {
  @Logger()
  logger: ILogger;

  @Inject()
  skillLoader: SkillLoaderService;

  @Inject()
  registry: SkillRegistryService;

  @InjectEntityModel(NetaClawSkillEntity)
  skillRepo: Repository<NetaClawSkillEntity>;

  private skillsDir: string;

  @Init()
  async init() {
    if ((process as any).pkg) {
      const root = resolveDataDir();
      this.skillsDir = path.resolve(root, 'skills');
    } else {
      this.skillsDir = path.resolve(process.cwd(), 'skills');
    }
    await fs.mkdir(this.skillsDir, { recursive: true });
  }

  /** 从 GitHub 安装 skill */
  async installFromGitHub(
    url: string,
    options?: { branch?: string; tag?: string },
    onProgress?: (step: string, percent: number) => void,
  ): Promise<SkillMeta> {
    // 1. 校验 URL
    const cleanUrl = url.replace(/\/$/, '');
    if (!SAFE_GITHUB_URL.test(cleanUrl)) {
      throw new Error('无效的 GitHub URL，仅支持 github.com 仓库');
    }

    onProgress?.('正在克隆仓库...', 10);

    // 2. Clone 到临时目录
    const tmpDir = path.join(this.skillsDir, `.tmp-${Date.now()}`);
    const ref = options?.tag || options?.branch || 'main';
    try {
      await execAsync(`git clone --depth 1 --branch ${ref} ${cleanUrl} "${tmpDir}"`, { timeout: 60000 });
    } catch (e: any) {
      try {
        await execAsync(`git clone --depth 1 ${cleanUrl} "${tmpDir}"`, { timeout: 60000 });
      } catch (e2: any) {
        throw new Error(`Git clone 失败: ${e2.message}`);
      }
    }

    onProgress?.('正在验证 SKILL.md...', 30);

    // 3. 验证 SKILL.md 存在
    const skillMdPath = path.join(tmpDir, 'SKILL.md');
    try {
      await fs.access(skillMdPath);
    } catch {
      await fs.rm(tmpDir, { recursive: true, force: true });
      throw new Error('仓库中未找到 SKILL.md 文件');
    }

    // 4. 解析 SKILL.md
    const raw = await fs.readFile(skillMdPath, 'utf-8');
    const skillMeta = this.skillLoader.parseSkillMd(raw);
    if (!skillMeta) {
      await fs.rm(tmpDir, { recursive: true, force: true });
      throw new Error('SKILL.md 解析失败：缺少 name 字段');
    }

    onProgress?.('正在安装...', 50);

    // 5. 路径安全检查 + 移动到 skills/{name}/
    const targetDir = path.join(this.skillsDir, skillMeta.name);
    const realSkillsDir = await fs.realpath(this.skillsDir);
    const realTarget = path.resolve(realSkillsDir, skillMeta.name);
    if (!realTarget.startsWith(realSkillsDir)) {
      await fs.rm(tmpDir, { recursive: true, force: true });
      throw new Error('路径安全检查失败');
    }

    // 移除旧版本（如果存在）
    await fs.rm(targetDir, { recursive: true, force: true });
    await fs.rename(tmpDir, targetDir);

    // 删除 .git 目录
    await fs.rm(path.join(targetDir, '.git'), { recursive: true, force: true });

    onProgress?.('正在获取 commit hash...', 60);

    // 6. 获取 commit hash
    let commitHash = '';
    try {
      const { stdout } = await execAsync(`git ls-remote ${cleanUrl} HEAD`, { timeout: 15000 });
      commitHash = stdout.split('\t')[0] || '';
    } catch { /* ignore */ }

    // 7. 计算指纹 + 写入 origin + 更新 lockfile
    const fingerprint = await this.registry.computeFingerprint(targetDir);
    const origin: SkillOrigin = {
      version: 1,
      source: 'github',
      url: cleanUrl,
      branch: ref,
      commitHash,
      installedAt: new Date().toISOString(),
    };
    await this.registry.writeOrigin(skillMeta.name, origin);

    const lockfile = await this.registry.readLockfile();
    lockfile.skills[skillMeta.name] = {
      version: skillMeta.version || '0.0.0',
      fingerprint,
      installedAt: Date.now(),
    };
    await this.registry.writeLockfile(lockfile);

    onProgress?.('正在同步数据库...', 80);

    // 8. 同步到数据库
    const existing = await this.skillRepo.findOneBy({ name: skillMeta.name });
    const entityData: Partial<NetaClawSkillEntity> = {
      name: skillMeta.name,
      label: skillMeta.name,
      description: skillMeta.description,
      skillType: (skillMeta.metadata as any)?.skillType ?? null,
      tags: (skillMeta.metadata as any)?.tags ?? null,
      version: skillMeta.version ?? null,
      source: 'github',
      sourceUrl: cleanUrl,
      installSpec: (skillMeta.metadata as any)?.install ?? null,
      metadata: skillMeta.metadata ?? null,
      installedAt: new Date(),
      fingerprint,
      status: 1,
    };
    if (existing) {
      await this.skillRepo.update({ name: skillMeta.name }, entityData);
    } else {
      await this.skillRepo.save(entityData as any);
    }

    // 9. 重新扫描
    await this.skillLoader.scanSkills();

    onProgress?.('安装完成', 100);

    // 10. 安装依赖（如果有）
    const installSpecs = (skillMeta.metadata as any)?.install;
    if (installSpecs && Array.isArray(installSpecs)) {
      await this.installDependencies(skillMeta.name, installSpecs);
    }

    return skillMeta;
  }

  /** 安装 skill 声明的依赖 */
  async installDependencies(
    skillName: string,
    installSpecs?: Record<string, unknown>[],
  ): Promise<{ success: boolean; logs: string[] }> {
    const logs: string[] = [];
    const skillDir = path.join(this.skillsDir, skillName);

    // 读取 skill.config.yaml 的 dependencies
    const configPath = path.join(skillDir, 'skill.config.yaml');
    let config: any = null;
    try {
      const raw = await fs.readFile(configPath, 'utf-8');
      const yamlLib = await import('js-yaml');
      config = yamlLib.load(raw);
    } catch { /* no config file */ }

    const deps = config?.dependencies;

    // Python: skill-level venv
    if (deps?.python) {
      try {
        const reqSource = deps.python.source || 'requirements.txt';
        const reqPath = path.join(skillDir, reqSource);
        await fs.access(reqPath);
        await execAsync(`uv venv .venv`, { cwd: skillDir, timeout: 60000 });
        const pip = process.platform === 'win32'
          ? path.join('.venv', 'Scripts', 'pip')
          : path.join('.venv', 'bin', 'pip');
        const { stdout } = await execAsync(`uv pip install -r ${reqSource} -p ${pip}`, {
          cwd: skillDir, timeout: 120000,
        });
        logs.push(`[python] venv created and deps installed: ${stdout.trim()}`);
      } catch (e: any) {
        logs.push(`[python] install failed: ${e.message}`);
      }
    }

    // Node: skill-level node_modules
    if (deps?.node?.packages?.length) {
      try {
        const pkgs = deps.node.packages.join(' ');
        const { stdout } = await execAsync(`npm install ${pkgs}`, {
          cwd: skillDir, timeout: 120000,
        });
        logs.push(`[node] installed: ${stdout.trim()}`);
      } catch (e: any) {
        logs.push(`[node] install failed: ${e.message}`);
      }
    }

    // Dotnet: restore
    if (deps?.dotnet?.project) {
      try {
        const { stdout } = await execAsync(`dotnet restore ${deps.dotnet.project}`, {
          cwd: skillDir, timeout: 120000,
        });
        logs.push(`[dotnet] restored: ${stdout.trim()}`);
      } catch (e: any) {
        logs.push(`[dotnet] restore failed: ${e.message}`);
      }
    }

    // System deps: check only
    if (deps?.system) {
      for (const dep of deps.system) {
        try {
          await execAsync(dep.check, { timeout: 10000 });
          logs.push(`[system] ${dep.name}: OK`);
        } catch {
          logs.push(`[system] ${dep.name}: NOT FOUND (run "${dep.check}" to verify)`);
        }
      }
    }

    // Setup script — 安全约束（Spec 10.9）
    const setupKey = process.platform === 'win32' ? 'win32' : 'posix';
    const setupScript = config?.setup?.[setupKey];
    if (setupScript) {
      if (setupScript.includes('..')) {
        logs.push(`[setup] 跳过: 路径包含 ".." (${setupScript})`);
      } else {
        const origin = await this.registry.readOrigin(skillName);
        const allowedSources = ['github', 'zip'];
        if (!origin || !allowedSources.includes(origin.source)) {
          logs.push(`[setup] 跳过: 仅 GitHub/ZIP 安装的 skill 允许执行 setup 脚本 (source=${origin?.source || 'local'})`);
        } else {
          try {
            const { stdout } = await execAsync(
              process.platform === 'win32' ? `powershell -File ${setupScript}` : `bash ${setupScript}`,
              { cwd: skillDir, timeout: 120000 },
            );
            logs.push(`[setup] executed ${setupScript}: ${stdout.trim()}`);
          } catch (e: any) {
            logs.push(`[setup] ${setupScript} failed: ${e.message}`);
          }
        }
      }
    }

    // Fallback: legacy installSpecs from SKILL.md metadata
    if (!config && installSpecs && Array.isArray(installSpecs)) {
      for (const spec of installSpecs) {
        const kind = spec.kind as string;
        const pkg = spec.package as string;
        if (!kind || !pkg) continue;
        try {
          if (kind === 'node' && SAFE_NODE_PACKAGE.test(pkg)) {
            const { stdout } = await execAsync(`npm install ${pkg}`, { cwd: skillDir, timeout: 120000 });
            logs.push(`[node-legacy] installed ${pkg}: ${stdout.trim()}`);
          } else if (kind === 'uv' && SAFE_UV_PACKAGE.test(pkg)) {
            await execAsync(`uv venv .venv`, { cwd: skillDir, timeout: 60000 });
            const { stdout } = await execAsync(`uv pip install ${pkg}`, { cwd: skillDir, timeout: 120000 });
            logs.push(`[uv-legacy] installed ${pkg}: ${stdout.trim()}`);
          }
        } catch (e: any) {
          logs.push(`[${kind}] install ${pkg} failed: ${e.message}`);
        }
      }
    }

    return { success: logs.every(l => !l.includes('failed')), logs };
  }

  /** 卸载 skill */
  async uninstall(name: string): Promise<void> {
    const targetDir = path.join(this.skillsDir, name);
    await fs.rm(targetDir, { recursive: true, force: true });
    await this.registry.removeOrigin(name);

    const lockfile = await this.registry.readLockfile();
    delete lockfile.skills[name];
    await this.registry.writeLockfile(lockfile);

    await this.skillRepo.delete({ name });
    await this.skillLoader.scanSkills();
  }

  /** 更新 skill（重新拉取） */
  async update(
    name: string,
    onProgress?: (step: string, percent: number) => void,
  ): Promise<SkillMeta> {
    const origin = await this.registry.readOrigin(name);
    if (!origin || origin.source !== 'github') {
      throw new Error(`Skill "${name}" 不是 GitHub 安装的，无法更新`);
    }
    return this.installFromGitHub(origin.url, { branch: origin.branch }, onProgress);
  }

  /** 检查所有 GitHub skill 是否有更新 */
  async checkUpdates(): Promise<Array<{ name: string; hasUpdate: boolean; currentHash: string; latestHash: string }>> {
    const results: Array<{ name: string; hasUpdate: boolean; currentHash: string; latestHash: string }> = [];
    const dbSkills = await this.skillRepo.find({ where: { source: 'github' } });

    for (const skill of dbSkills) {
      const origin = await this.registry.readOrigin(skill.name);
      if (!origin) continue;

      try {
        const { stdout } = await execAsync(`git ls-remote ${origin.url} HEAD`, { timeout: 15000 });
        const latestHash = stdout.split('\t')[0] || '';
        results.push({
          name: skill.name,
          hasUpdate: latestHash !== origin.commitHash,
          currentHash: origin.commitHash,
          latestHash,
        });
      } catch {
        results.push({ name: skill.name, hasUpdate: false, currentHash: origin.commitHash, latestHash: '' });
      }
    }
    return results;
  }

  /** 从 ZIP 文件安装 skill */
  async installFromZip(
    zipBuffer: Buffer,
    onProgress?: (step: string, percent: number) => void,
  ): Promise<SkillMeta> {
    // 1. 大小校验
    if (zipBuffer.length > MAX_ZIP_SIZE) {
      throw new Error(`ZIP 文件过大（${(zipBuffer.length / 1024 / 1024).toFixed(1)}MB），限制 50MB`);
    }

    onProgress?.('正在解压 ZIP...', 10);

    // 2. 解压并校验
    let zip: AdmZip;
    try {
      zip = new AdmZip(zipBuffer);
    } catch {
      throw new Error('无效的 ZIP 文件');
    }

    const entries = zip.getEntries();

    // 3. 查找 SKILL.md（根目录或一级子目录）
    let skillMdEntry = entries.find(e => e.entryName === 'SKILL.md');
    let stripPrefix = '';
    if (!skillMdEntry) {
      // 尝试一级子目录: xxx/SKILL.md
      skillMdEntry = entries.find(e => {
        const parts = e.entryName.replace(/\\/g, '/').split('/');
        return parts.length === 2 && parts[1] === 'SKILL.md';
      });
      if (skillMdEntry) {
        stripPrefix = skillMdEntry.entryName.replace(/\\/g, '/').split('/')[0] + '/';
      }
    }
    if (!skillMdEntry) {
      throw new Error('ZIP 中未找到 SKILL.md 文件');
    }

    onProgress?.('正在解析 SKILL.md...', 30);

    // 4. 解析 SKILL.md
    const skillMdContent = skillMdEntry.getData().toString('utf-8');
    const skillMeta = this.skillLoader.parseSkillMd(skillMdContent);
    if (!skillMeta) {
      throw new Error('SKILL.md 解析失败：缺少 name 字段');
    }

    onProgress?.('正在写入文件...', 50);

    // 5. 路径安全检查
    const targetDir = path.join(this.skillsDir, skillMeta.name);
    const realSkillsDir = await fs.realpath(this.skillsDir);
    const realTarget = path.resolve(realSkillsDir, skillMeta.name);
    if (!realTarget.startsWith(realSkillsDir)) {
      throw new Error('路径安全检查失败');
    }

    // 6. 清理旧版本 + 解压文件
    await fs.rm(targetDir, { recursive: true, force: true });
    await fs.mkdir(targetDir, { recursive: true });

    for (const entry of entries) {
      if (entry.isDirectory) continue;

      // 去掉前缀目录
      let relPath = entry.entryName.replace(/\\/g, '/');
      if (stripPrefix && relPath.startsWith(stripPrefix)) {
        relPath = relPath.slice(stripPrefix.length);
      } else if (stripPrefix) {
        continue; // 不属于 skill 目录的文件，跳过
      }

      // 路径遍历防护
      if (!relPath || relPath.startsWith('/') || relPath.includes('..')) {
        this.logger.warn(`跳过不安全的 ZIP 路径: ${entry.entryName}`);
        continue;
      }

      // 单文件大小限制
      if (entry.header.size > MAX_SINGLE_FILE) {
        this.logger.warn(`跳过过大文件: ${relPath} (${(entry.header.size / 1024).toFixed(0)}KB)`);
        continue;
      }

      // 跳过 .git 目录
      if (relPath.startsWith('.git/') || relPath === '.git') continue;

      const destPath = path.join(targetDir, relPath);
      // 二次路径安全检查
      if (!path.resolve(destPath).startsWith(realTarget)) {
        this.logger.warn(`路径逃逸，跳过: ${relPath}`);
        continue;
      }

      await fs.mkdir(path.dirname(destPath), { recursive: true });
      await fs.writeFile(destPath, entry.getData());
    }

    onProgress?.('正在同步数据库...', 80);

    // 7. 计算指纹 + 写入 origin + 更新 lockfile
    const fingerprint = await this.registry.computeFingerprint(targetDir);
    const origin: SkillOrigin = {
      version: 1,
      source: 'zip',
      url: '',
      branch: '',
      commitHash: '',
      installedAt: new Date().toISOString(),
    };
    await this.registry.writeOrigin(skillMeta.name, origin);

    const lockfile = await this.registry.readLockfile();
    lockfile.skills[skillMeta.name] = {
      version: skillMeta.version || '0.0.0',
      fingerprint,
      installedAt: Date.now(),
    };
    await this.registry.writeLockfile(lockfile);

    // 8. 同步到数据库
    const existing = await this.skillRepo.findOneBy({ name: skillMeta.name });
    const entityData: Partial<NetaClawSkillEntity> = {
      name: skillMeta.name,
      label: skillMeta.name,
      description: skillMeta.description,
      skillType: (skillMeta.metadata as any)?.skillType ?? null,
      tags: (skillMeta.metadata as any)?.tags ?? null,
      version: skillMeta.version ?? null,
      source: 'zip',
      sourceUrl: null,
      installSpec: (skillMeta.metadata as any)?.install ?? null,
      metadata: skillMeta.metadata ?? null,
      installedAt: new Date(),
      fingerprint,
      status: 1,
    };
    if (existing) {
      await this.skillRepo.update({ name: skillMeta.name }, entityData);
    } else {
      await this.skillRepo.save(entityData as any);
    }

    // 9. 重新扫描
    await this.skillLoader.scanSkills();

    onProgress?.('安装完成', 100);

    // 10. 安装依赖（如果有）
    const installSpecs = (skillMeta.metadata as any)?.install;
    if (installSpecs && Array.isArray(installSpecs)) {
      await this.installDependencies(skillMeta.name, installSpecs);
    }

    return skillMeta;
  }
}
