/**
 * RepoMap 主类
 * 使用 AST 分析和 PageRank 算法生成代码仓库地图
 */

import * as fs from 'fs/promises';
import * as path from 'path';
import { TagExtractor } from './tags/extractor.js';
import { pagerank, distributeRanksToDefinitions } from './ranking/pagerank.js';
import { Graph } from './ranking/graph.js';
import { DiskCache } from './cache/disk-cache.js';
import type { Tag, RepoMapConfig, TagCacheEntry } from './types.js';

/**
 * RepoMap 配置默认值
 */
const defaultConfig: RepoMapConfig = {
  mapTokens: 1024,
  mapMulNoFiles: 8,
  maxContextWindow: 128000,
  refresh: 'auto',
  cacheDir: '.ai-assist/tags-cache',
  verbose: false,
  exclude: [
    'node_modules/**',
    'dist/**',
    'build/**',
    '.git/**',
    '*.test.*',
    '*.spec.*',
    '**/*.d.ts',
  ],
  include: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py'],
};

/**
 * RepoMap 类
 * 生成代码仓库的上下文地图，帮助 AI 理解代码结构
 */
export class RepoMap {
  private tagExtractor: TagExtractor;
  private tagsCache: DiskCache<TagCacheEntry>;
  private config: RepoMapConfig;
  private root: string;

  constructor(root: string, config: Partial<RepoMapConfig> = {}) {
    this.root = root;
    this.config = { ...defaultConfig, ...config };

    this.tagExtractor = new TagExtractor();
    this.tagsCache = new DiskCache(
      path.join(this.root, this.config.cacheDir)
    );
  }

  /**
   * 获取 repo map
   * @param chatFiles 当前对话中涉及的文件
   * @param otherFiles 仓库中其他文件
   * @param mentionedFnames 对话中提到的文件名
   * @param mentionedIdents 对话中提到的标识符
   */
  async getRepoMap(
    chatFiles: string[],
    otherFiles: string[],
    mentionedFnames: Set<string> = new Set(),
    mentionedIdents: Set<string> = new Set()
  ): Promise<string> {
    if (this.config.mapTokens <= 0 || otherFiles.length === 0) {
      return '';
    }

    let maxMapTokens = this.config.mapTokens;

    // 无聊天文件时，给更大的视图
    if (chatFiles.length === 0) {
      maxMapTokens = Math.min(
        maxMapTokens * this.config.mapMulNoFiles,
        this.config.maxContextWindow - 4096
      );
    }

    const rankedTags = await this.getRankedTags(
      chatFiles,
      otherFiles,
      mentionedFnames,
      mentionedIdents
    );

    // 二分搜索找到最优 token 数量
    return this.fitToTokenLimit(rankedTags, maxMapTokens, new Set(chatFiles));
  }

  /**
   * 获取排序后的 tags
   */
  private async getRankedTags(
    chatFnames: string[],
    otherFnames: string[],
    mentionedFnames: Set<string>,
    mentionedIdents: Set<string>
  ): Promise<Tag[]> {
    // ident -> files that define it
    const defines = new Map<string, Set<string>>();
    // ident -> files that reference it
    const references = new Map<string, string[]>();
    // (file:ident) -> tag objects
    const definitions = new Map<string, Tag[]>();
    // personalization vector for PageRank
    const personalization = new Map<string, number>();

    const allFnames = [...new Set([...chatFnames, ...otherFnames])];
    const chatRelFnames = new Set(chatFnames.map((f) => this.getRelFname(f)));
    const basePersonalize = 100 / Math.max(allFnames.length, 1);

    // 收集所有文件的 tags
    for (const fname of allFnames) {
      const relFname = this.getRelFname(fname);
      let currentPers = 0;

      // 个性化权重
      if (chatFnames.includes(fname)) {
        currentPers += basePersonalize;
      }
      if (mentionedFnames.has(relFname)) {
        currentPers = Math.max(currentPers, basePersonalize);
      }

      // 路径组件匹配
      const pathParts = relFname.split('/');
      const basename = pathParts[pathParts.length - 1];
      const basenameNoExt = basename.replace(/\.[^.]+$/, '');
      const components = new Set([...pathParts, basename, basenameNoExt]);

      if ([...components].some((c) => mentionedIdents.has(c))) {
        currentPers += basePersonalize;
      }

      if (currentPers > 0) {
        personalization.set(relFname, currentPers);
      }

      // 提取 tags
      const tags = await this.getTags(fname, relFname);
      for (const tag of tags) {
        if (tag.kind === 'def') {
          if (!defines.has(tag.name)) defines.set(tag.name, new Set());
          defines.get(tag.name)!.add(relFname);

          const key = `${relFname}:${tag.name}`;
          if (!definitions.has(key)) definitions.set(key, []);
          definitions.get(key)!.push(tag);
        } else {
          if (!references.has(tag.name)) references.set(tag.name, []);
          references.get(tag.name)!.push(relFname);
        }
      }
    }

    // 构建图
    const graph = new Graph();

    // 找到同时有定义和引用的标识符
    const idents = [...defines.keys()].filter((id) => references.has(id));

    for (const ident of idents) {
      const definers = defines.get(ident)!;
      const refs = references.get(ident)!;

      // 计算权重乘数
      let mul = 1.0;
      if (mentionedIdents.has(ident)) mul *= 10;
      if (this.isSignificantName(ident)) mul *= 10;
      if (ident.startsWith('_')) mul *= 0.1;
      if (definers.size > 5) mul *= 0.1;

      // 统计引用次数
      const refCounts = new Map<string, number>();
      for (const ref of refs) {
        refCounts.set(ref, (refCounts.get(ref) || 0) + 1);
      }

      // 添加边
      for (const [referencer, numRefs] of refCounts) {
        for (const definer of definers) {
          let useMul = mul;
          if (chatRelFnames.has(referencer)) useMul *= 50;

          graph.addEdge({
            from: referencer,
            to: definer,
            weight: useMul * Math.sqrt(numRefs),
            ident,
          });
        }
      }
    }

    // 运行 PageRank
    const ranked = pagerank(graph, { personalization });

    // 分配排名到定义
    const rankedDefinitions = distributeRanksToDefinitions(graph, ranked);

    // 排序
    const sorted = [...rankedDefinitions.entries()].sort((a, b) => b[1] - a[1]);

    // 收集排序后的 tags
    const rankedTags: Tag[] = [];
    for (const [key] of sorted) {
      const colonIdx = key.lastIndexOf(':');
      const fname = key.substring(0, colonIdx);
      if (chatRelFnames.has(fname)) continue;
      const tags = definitions.get(key) || [];
      rankedTags.push(...tags);
    }

    return rankedTags;
  }

  /**
   * 获取文件的 tags（带缓存）
   */
  private async getTags(fname: string, relFname: string): Promise<Tag[]> {
    const mtime = await this.getMtime(fname);
    if (!mtime) return [];

    // 检查缓存
    const cached = await this.tagsCache.get(fname);
    if (cached && cached.mtime === mtime) {
      return cached.data;
    }

    // 提取 tags
    const tags = await this.tagExtractor.getTags(fname, relFname);

    // 更新缓存
    await this.tagsCache.set(fname, { mtime, data: tags });

    return tags;
  }

  /**
   * 二分搜索拟合 token 限制
   */
  private fitToTokenLimit(
    tags: Tag[],
    maxTokens: number,
    chatRelFnames: Set<string>
  ): string {
    const numTags = tags.length;
    if (numTags === 0) return '';

    let lower = 0;
    let upper = numTags;
    let bestTree = '';
    let bestTokens = 0;

    let middle = Math.min(Math.floor(maxTokens / 25), numTags);

    while (lower <= upper) {
      const tree = this.toTree(tags.slice(0, middle), chatRelFnames);
      const numTokens = this.tokenCount(tree);

      const pctErr = Math.abs(numTokens - maxTokens) / maxTokens;

      if ((numTokens <= maxTokens && numTokens > bestTokens) || pctErr < 0.15) {
        bestTree = tree;
        bestTokens = numTokens;

        if (pctErr < 0.15) break;
      }

      if (numTokens < maxTokens) {
        lower = middle + 1;
      } else {
        upper = middle - 1;
      }

      middle = Math.floor((lower + upper) / 2);
    }

    return bestTree;
  }

  /**
   * 转换为树形展示
   */
  private toTree(tags: Tag[], chatRelFnames: Set<string>): string {
    if (tags.length === 0) return '';

    const output: string[] = [];
    let curFname: string | null = null;
    let lois: number[] = [];
    let curNames: string[] = [];

    // 按文件分组
    const sortedTags = [...tags].sort(
      (a, b) => a.relFname.localeCompare(b.relFname) || a.line - b.line
    );

    for (const tag of sortedTags) {
      if (chatRelFnames.has(tag.relFname)) continue;

      if (tag.relFname !== curFname) {
        // 输出前一个文件
        if (curFname && (lois.length > 0 || curNames.length > 0)) {
          output.push(this.renderFileTree(curFname, lois, curNames));
        }
        curFname = tag.relFname;
        lois = [];
        curNames = [];
      }

      if (tag.line >= 0) {
        lois.push(tag.line);
      }
      curNames.push(tag.name);
    }

    // 输出最后一个文件
    if (curFname && (lois.length > 0 || curNames.length > 0)) {
      output.push(this.renderFileTree(curFname, lois, curNames));
    }

    return output.join('\n');
  }

  /**
   * 渲染单个文件的树形展示
   */
  private renderFileTree(
    relFname: string,
    lois: number[],
    names: string[]
  ): string {
    const uniqueNames = [...new Set(names)];
    const uniqueLines = [...new Set(lois)].sort((a, b) => a - b);

    const lines = [`${relFname}:`];

    // 简化版：显示符号列表
    for (const name of uniqueNames.slice(0, 10)) {
      lines.push(`  - ${name}`);
    }

    if (uniqueNames.length > 10) {
      lines.push(`  ... and ${uniqueNames.length - 10} more`);
    }

    return lines.join('\n');
  }

  /**
   * 判断是否是有意义的名称
   */
  private isSignificantName(name: string): boolean {
    if (name.length < 8) return false;
    const hasAlpha = /[a-zA-Z]/.test(name);
    const isSnake = name.includes('_') && hasAlpha;
    const isKebab = name.includes('-') && hasAlpha;
    const isCamel = /[a-z]/.test(name) && /[A-Z]/.test(name);
    return isSnake || isKebab || isCamel;
  }

  /**
   * 获取相对路径
   */
  private getRelFname(fname: string): string {
    if (fname.startsWith(this.root)) {
      return fname.slice(this.root.length + 1);
    }
    return fname;
  }

  /**
   * 获取文件修改时间
   */
  private async getMtime(fname: string): Promise<number | null> {
    try {
      const stat = await fs.stat(fname);
      return stat.mtimeMs;
    } catch {
      return null;
    }
  }

  /**
   * 估算 token 数量
   * 简化估算：约 4 字符一个 token
   */
  private tokenCount(text: string): number {
    return Math.ceil(text.length / 4);
  }

  /**
   * 刷新缓存到磁盘
   */
  async flushCache(): Promise<void> {
    await this.tagsCache.flushAll();
  }

  /**
   * 清空缓存
   */
  async clearCache(): Promise<void> {
    await this.tagsCache.clear();
  }
}

/**
 * 创建 RepoMap 实例
 */
export function createRepoMap(
  root: string,
  config?: Partial<RepoMapConfig>
): RepoMap {
  return new RepoMap(root, config);
}
