import { Provide, Scope, ScopeEnum, Logger, Init, Inject } from '@midwayjs/core';
import { SkillConfigService } from './skill_config.js';
import { buildSkillEnvSchema, isSkillEnvSchemaEqual } from './skill_env_schema.js';
import { ILogger } from '@midwayjs/logger';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as yaml from 'js-yaml';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawSkillEntity } from '../entity/skill.js';
import { resolveDataDir } from '../../../comm/data-dir.js';
import type { SkillConfig, SkillClassification, SkillInterfaceField } from './skill_config.js';

export interface SkillDiagnostic {
  level: 'error' | 'warning' | 'info';
  code: string;
  skillName: string;
  message: string;
  path?: string;
  detail?: Record<string, unknown>;
}

export interface SkillMeta {
  name: string;
  description: string;
  version?: string;
  metadata?: Record<string, unknown>;
  content: string;
  /** skill 目录下除 SKILL.md 外的附属文件相对路径列表 */
  files: string[];
}

export interface SkillEntry {
  meta: SkillMeta;
  dbRecord?: NetaClawSkillEntity;
}

const MAX_NAME_LENGTH = 64;

export function validateSkillName(name: string, parentDirName?: string): string[] {
  const errors: string[] = [];
  if (!name) { errors.push('name is required'); return errors; }
  if (name.length > MAX_NAME_LENGTH) errors.push(`name exceeds ${MAX_NAME_LENGTH} characters`);
  if (!/^[a-z0-9-]+$/.test(name)) errors.push('name must be lowercase a-z, 0-9, hyphens only');
  if (name.startsWith('-') || name.endsWith('-')) errors.push('name must not start or end with hyphen');
  if (name.includes('--')) errors.push('name must not contain consecutive hyphens');
  if (parentDirName && name !== parentDirName) errors.push(`name "${name}" does not match directory "${parentDirName}"`);
  return errors;
}

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

  private skills: Map<string, SkillMeta> = new Map();
  private skillsDir: string;

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

  @Inject()
  skillConfig: SkillConfigService;

  private diagnostics: SkillDiagnostic[] = [];

  getDiagnostics(level?: string): SkillDiagnostic[] {
    if (level) return this.diagnostics.filter(d => d.level === level);
    return [...this.diagnostics];
  }

  private addDiagnostic(diag: SkillDiagnostic): void {
    this.diagnostics.push(diag);
  }

  @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 });
    await this.scanSkills();
  }

  private static readonly MAX_SKILLS = 200;
  private static readonly MAX_SKILL_FILE_BYTES = 256 * 1024;
  private static readonly MAX_SKILLS_PROMPT_CHARS = 30_000;

  async scanSkills(): Promise<void> {
    this.skills.clear();
    this.skillConfig.clearAll();
    this.diagnostics = [];
    const realPathSet = new Set<string>();
    try {
      const entries = await fs.readdir(this.skillsDir, { withFileTypes: true });
      let loaded = 0;
      for (const entry of entries) {
        if (!entry.isDirectory()) continue;
        if (loaded >= SkillLoaderService.MAX_SKILLS) {
          this.logger.warn('[SkillLoader] 已达 %d 个 Skill 上限，跳过剩余', SkillLoaderService.MAX_SKILLS);
          break;
        }
        // symlink 去重
        const skillDirPath = path.join(this.skillsDir, entry.name);
        let realPath: string;
        try {
          realPath = await fs.realpath(skillDirPath);
        } catch {
          realPath = skillDirPath;
        }
        if (realPathSet.has(realPath)) continue;
        realPathSet.add(realPath);
        const skillMdPath = path.join(this.skillsDir, entry.name, 'SKILL.md');
        try {
          const raw = await fs.readFile(skillMdPath, 'utf-8');
          if (Buffer.byteLength(raw) > SkillLoaderService.MAX_SKILL_FILE_BYTES) {
            this.logger.warn('[SkillLoader] SKILL.md 超过 256KB 限制: %s', entry.name);
            continue;
          }
          const skill = this.parseSkillMd(raw);
          if (skill) {
            // 名称验证诊断
            const nameErrors = validateSkillName(skill.name, entry.name);
            for (const err of nameErrors) {
              if (err.includes('does not match')) {
                this.addDiagnostic({ level: 'warning', code: 'NAME_MISMATCH', skillName: skill.name, message: err, path: skillMdPath });
              } else {
                this.addDiagnostic({ level: 'warning', code: 'NAME_INVALID', skillName: skill.name, message: err, path: skillMdPath });
              }
            }

            // 碰撞检测
            if (this.skills.has(skill.name)) {
              this.addDiagnostic({
                level: 'warning', code: 'NAME_COLLISION', skillName: skill.name,
                message: `Name collision: "${skill.name}" already loaded`,
                detail: { loserPath: skillMdPath },
              });
              continue;
            }

            // description 检查
            if (!skill.description) {
              this.addDiagnostic({ level: 'error', code: 'DESC_MISSING', skillName: skill.name, message: 'Missing description', path: skillMdPath });
              continue;
            }
            if (skill.description.length > 1024) {
              this.addDiagnostic({ level: 'warning', code: 'DESC_TOO_LONG', skillName: skill.name, message: `Description exceeds 1024 chars (${skill.description.length})`, path: skillMdPath });
            }

            // 递归收集附属文件
            const skillDir = path.join(this.skillsDir, entry.name);
            skill.files = await this.collectFiles(skillDir, skillDir);
            this.skills.set(skill.name, skill);
            loaded++;
            await this.skillConfig.loadConfig(skillDir, skill.name);
            this.logger.info('[SkillLoader] 已加载 Skill: %s (%d 个附属文件)', skill.name, skill.files.length);
          }
        } catch {
          // SKILL.md 不存在或解析失败，跳过
        }
      }
      // 扫描完成后检查 compute skill 状态
      for (const [name] of this.skills) {
        const config = this.skillConfig.getConfig(name);
        if (!config) {
          const parseErr = this.skillConfig.getParseError(name);
          if (parseErr) {
            this.addDiagnostic({ level: 'error', code: 'CONFIG_PARSE_ERROR', skillName: name, message: `skill.config.yaml parse failed: ${parseErr}` });
          }
          continue;
        }

        // Python venv 检查
        if (config.runtime === 'python') {
          const venvPath = path.join(this.skillsDir, name, '.venv');
          try { await fs.access(venvPath); } catch {
            this.addDiagnostic({ level: 'warning', code: 'VENV_MISSING', skillName: name, message: 'Python .venv not found, run dependency install' });
          }
        }

        // env 配置检查
        if (config.env) {
          const entity = await this.skillRepo.findOneBy({ name });
          const hasSecrets = !!entity?.secrets;
          for (const envItem of config.env) {
            if (envItem.required && !hasSecrets) {
              this.addDiagnostic({ level: 'warning', code: 'ENV_NOT_CONFIGURED', skillName: name, message: `Required env "${envItem.name}" not configured` });
              break;
            }
          }
        }
      }
      this.logger.info('[SkillLoader] 共加载 %d 个 Skill', this.skills.size);
    } catch {
      this.logger.warn('[SkillLoader] Skills 目录不存在: %s', this.skillsDir);
    }
  }

  parseSkillMd(raw: string): SkillMeta | null {
    const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
    if (!fmMatch) return null;

    try {
      const frontmatter = yaml.load(fmMatch[1]) as Record<string, unknown>;
      const content = fmMatch[2].trim();
      const name = frontmatter.name as string;
      if (!name) return null;

      const nameErrors = validateSkillName(name);
      if (nameErrors.length > 0) {
        this.logger.warn('[SkillLoader] Skill "%s" name validation: %s', name, nameErrors.join(', '));
      }

      return {
        name,
        description: (frontmatter.description as string) ?? '',
        version: (frontmatter.version as string) ?? undefined,
        metadata: (frontmatter.metadata as Record<string, unknown>) ?? undefined,
        content,
        files: [],
      };
    } catch (e) {
      this.logger.warn('[SkillLoader] YAML 解析失败: %s', e);
      return null;
    }
  }

  getSkill(name: string): SkillMeta | undefined {
    return this.skills.get(name);
  }

  getAllSkills(): SkillMeta[] {
    return Array.from(this.skills.values());
  }

  getSkillContent(name: string): string | null {
    const skill = this.skills.get(name);
    return skill ? skill.content : null;
  }

  getSkillsDir(): string {
    return this.skillsDir;
  }

  filterByConditions(skillNames: string[], availableTools: string[]): SkillMeta[] {
    const result: SkillMeta[] = [];
    for (const name of skillNames) {
      const skill = this.skills.get(name);
      if (!skill) continue;

      const conditions = (skill.metadata as any)?.conditions;
      if (conditions) {
        if (conditions.requires_tools) {
          const required = conditions.requires_tools as string[];
          if (!required.every(t => availableTools.includes(t))) continue;
        }
        if (conditions.fallback_for_tools) {
          const fallbackFor = conditions.fallback_for_tools as string[];
          if (fallbackFor.every(t => availableTools.includes(t))) continue;
        }
      }
      result.push(skill);
    }
    return result;
  }

  buildSkillsPrompt(skillNames: string[], availableTools: string[]): string {
    const filtered = this.filterByConditions(skillNames, availableTools);
    if (filtered.length === 0) return '';

    const lines: string[] = [];
    let totalChars = 0;

    for (const s of filtered) {
      const fm = s.metadata as any;
      if (fm?.['disable-model-invocation'] === true) continue;

      const classification = this.skillConfig.classify(s.name);
      const category = fm?.category || '通用';
      const tags = Array.isArray(fm?.tags) ? fm.tags.join(',') : '';

      let body = `    ${s.description}`;

      if (classification === 'compute-entry') {
        const config = this.skillConfig.getConfig(s.name);
        if (config?.interface?.input) {
          const inputDesc = Object.entries(config.interface.input)
            .map(([k, v]: [string, SkillInterfaceField]) => `${k}(${v.type}${v.required ? ',必填' : ''}${v.default ? ',默认' + v.default : ''})`)
            .join(', ');
          body += `\n    输入: ${inputDesc}`;
        }
      }

      const line = `  <skill name="${s.name}" type="${classification}" category="${category}" tags="${tags}">\n${body}\n  </skill>`;
      if (totalChars + line.length > SkillLoaderService.MAX_SKILLS_PROMPT_CHARS) break;
      lines.push(line);
      totalChars += line.length;
    }

    if (lines.length === 0) return '';

    return `\n\n## 技能（必须扫描）\n回复前，扫描以下技能列表。prompt 类型用 read_skill 加载指令，compute-entry 类型用 execute_skill 直接调用，compute-toolkit 类型用 read_skill 加载指令后通过 bash 执行脚本。\n读取 skill 后，如果返回中包含 <skill_directory>，执行该 skill 的脚本时必须先 cd 到该目录。\n如果返回中包含 <skill_required_references>，你必须在执行任何操作前先用 read_skill_file 逐一读取列出的所有文档。这不是建议，是强制要求。\n\n<available_skills>\n${lines.join('\n')}\n</available_skills>`;
  }

  async getSkillMetas(): Promise<any[]> {
    const fileSkills = this.getAllSkills();
    const dbSkills = await this.skillRepo.find();
    const dbMap = new Map(dbSkills.map(s => [s.name, s]));

    const result = [];
    for (const fs of fileSkills) {
      let dbRecord = dbMap.get(fs.name);
      const config = this.skillConfig.getConfig(fs.name);
      const envSchema = buildSkillEnvSchema(config ?? {});
      if (!dbRecord) {
        dbRecord = await this.skillRepo.save({
          name: fs.name,
          label: fs.name,
          description: fs.description,
          skillType: (fs.metadata as any)?.skillType ?? null,
          tags: (fs.metadata as any)?.tags ?? null,
          version: fs.version ?? null,
          source: 'local',
          metadata: fs.metadata ?? null,
          status: 1,
          envSchema,
        } as any);
      } else if (!isSkillEnvSchemaEqual(dbRecord.envSchema, envSchema)) {
        dbRecord.envSchema = envSchema as any;
        await this.skillRepo.save(dbRecord);
      }
      result.push({
        name: fs.name,
        label: dbRecord.label || fs.name,
        description: fs.description || dbRecord.description,
        skillType: (fs.metadata as any)?.skillType ?? dbRecord.skillType,
        tags: (fs.metadata as any)?.tags ?? dbRecord.tags,
        emoji: (fs.metadata as any)?.emoji ?? null,
        status: dbRecord.status,
        version: fs.version ?? dbRecord.version,
        source: dbRecord.source ?? 'local',
        sourceUrl: dbRecord.sourceUrl,
        installedAt: dbRecord.installedAt,
        metadata: fs.metadata,
        classification: this.skillConfig.classify(fs.name),
      });
    }
    return result;
  }

  async setSkillStatus(name: string, status: number): Promise<void> {
    const existing = await this.skillRepo.findOneBy({ name });
    if (existing) {
      await this.skillRepo.update({ name }, { status });
    } else {
      await this.skillRepo.save({ name, label: name, status } as any);
    }
  }

  /** 递归收集 skill 目录下除 SKILL.md 外的所有文件，返回相对路径列表 */
  private async collectFiles(baseDir: string, currentDir: string): Promise<string[]> {
    const INFRA_FILES = new Set([
      'skill.config.yaml', 'requirements.txt', 'package.json',
      'package-lock.json', 'tsconfig.json', '.env',
    ]);
    const results: string[] = [];
    try {
      const entries = await fs.readdir(currentDir, { withFileTypes: true });
      for (const entry of entries) {
        const fullPath = path.join(currentDir, entry.name);
        if (entry.isDirectory()) {
          // 跳过 .git / .skillhub / node_modules
          if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
          const sub = await this.collectFiles(baseDir, fullPath);
          results.push(...sub);
        } else if (entry.name !== 'SKILL.md' && !INFRA_FILES.has(entry.name)) {
          const rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
          results.push(rel);
        }
      }
    } catch {
      // 目录不可读，跳过
    }
    return results;
  }

  /** 获取 skill 附属文件列表 */
  getSkillFiles(name: string): string[] {
    const skill = this.skills.get(name);
    return skill ? skill.files : [];
  }

  /** 获取 skill 附属文件的绝对路径（含安全校验） */
  getSkillFilePath(skillName: string, relativePath: string): string | null {
    const skill = this.skills.get(skillName);
    if (!skill) return null;
    // 拒绝路径穿越
    if (relativePath.includes('..')) return null;
    const skillDir = path.join(this.skillsDir, skillName);
    const resolved = path.resolve(skillDir, relativePath);
    // 确保解析后的路径仍在 skill 目录内
    if (!resolved.startsWith(path.resolve(skillDir) + path.sep)) return null;
    // 确保文件在已知列表中
    const normalized = relativePath.replace(/\\/g, '/');
    if (!skill.files.includes(normalized)) return null;
    return resolved;
  }

  getSkillConfig(name: string): SkillConfig | null {
    return this.skillConfig.getConfig(name);
  }

  getSkillClassification(name: string): SkillClassification {
    return this.skillConfig.classify(name);
  }

  resolveSkillByPath(absPath: string): string | null {
    if (!absPath) return null;
    const normalized = path.resolve(absPath);
    const skillsDirNorm = path.resolve(this.skillsDir);
    if (!normalized.startsWith(skillsDirNorm + path.sep)) return null;
    const relative = normalized.slice(skillsDirNorm.length + 1);
    const skillName = relative.split(path.sep)[0];
    if (!skillName || !this.skills.has(skillName)) return null;
    return skillName;
  }
}
