import { createHash } from "node:crypto";
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import type { ProjectContextSnapshot, ProjectSummary, WorkspaceSkillSummary } from "@qjclaw/shared-types";
import type { ProjectStoreService } from "./project-store.js";

const MAX_MEMORY_FILES = 8;
const MAX_MEMORY_SNIPPET_LENGTH = 600;
const MAX_MEMORY_SUMMARY_LENGTH = 3200;
const MAX_TRACKED_MEMORY_FILES = 64;
const TEXT_FILE_PATTERN = /\.(md|txt|json|ya?ml)$/i;
const CONTEXT_ROOT_FILES = ["project.json", "SOUL.md", "USER.md", "README.md"] as const;

interface ProjectContextCacheEntry {
  snapshot: ProjectContextSnapshot;
  stateHash: string;
}

interface TrackedFileState {
  relativePath: string;
  size: number;
  mtimeMs: number;
}

interface ProjectContextBuildState {
  project: ProjectSummary;
  projectRoot: string;
  skills: WorkspaceSkillSummary[];
  stateHash: string;
}

function normalizeText(content: string): string {
  return content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").trim();
}

function truncateText(content: string, limit: number): string {
  if (content.length <= limit) {
    return content;
  }
  return `${content.slice(0, limit).trimEnd()}...`;
}

async function pathExists(targetPath: string): Promise<boolean> {
  try {
    await stat(targetPath);
    return true;
  } catch {
    return false;
  }
}

export class ProjectContextService {
  private readonly projectStore: ProjectStoreService;
  private readonly snapshotCache = new Map<string, ProjectContextCacheEntry>();
  private readonly dirtyProjects = new Set<string>();

  constructor(projectStore: ProjectStoreService) {
    this.projectStore = projectStore;
  }

  async getSnapshot(projectId: string): Promise<ProjectContextSnapshot> {
    const buildState = await this.loadBuildState(projectId);
    const cached = this.snapshotCache.get(projectId);
    if (cached && cached.stateHash === buildState.stateHash && !this.dirtyProjects.has(projectId)) {
      return cached.snapshot;
    }

    const nextSnapshot = await this.buildSnapshot(buildState);
    this.snapshotCache.set(projectId, {
      snapshot: nextSnapshot,
      stateHash: buildState.stateHash
    });
    this.dirtyProjects.delete(projectId);
    return nextSnapshot;
  }

  async refreshSnapshot(projectId: string): Promise<ProjectContextSnapshot> {
    const buildState = await this.loadBuildState(projectId);
    const nextSnapshot = await this.buildSnapshot(buildState);
    this.snapshotCache.set(projectId, {
      snapshot: nextSnapshot,
      stateHash: buildState.stateHash
    });
    this.dirtyProjects.delete(projectId);
    return nextSnapshot;
  }

  invalidateSnapshot(projectId: string): void {
    this.dirtyProjects.add(projectId);
  }

  async buildSystemContext(projectId: string): Promise<string> {
    const snapshot = await this.getSnapshot(projectId);
    return this.renderSystemContext(snapshot);
  }

  private async loadBuildState(projectId: string): Promise<ProjectContextBuildState> {
    const [project, projectRoot, skills] = await Promise.all([
      this.projectStore.getProjectSummary(projectId),
      this.projectStore.getProjectRoot(projectId),
      this.projectStore.listProjectSkills(projectId)
    ]);

    const stateHash = await this.computeStateHash(project, projectRoot, skills);
    return {
      project,
      projectRoot,
      skills,
      stateHash
    };
  }

  private async computeStateHash(
    project: ProjectSummary,
    projectRoot: string,
    skills: WorkspaceSkillSummary[]
  ): Promise<string> {
    const trackedRootStates = await Promise.all(
      CONTEXT_ROOT_FILES.map((fileName) => this.readTrackedFileState(projectRoot, path.join(projectRoot, fileName)))
    );
    const memoryStates = await this.collectTrackedFileStates(path.join(projectRoot, "memory"));
    const statePayload = JSON.stringify({
      project: {
        id: project.id,
        name: project.name,
        description: project.description ?? null,
        version: project.version ?? null,
        ready: project.ready,
        updatedAt: project.updatedAt
      },
      skills: skills.map((skill) => ({
        id: skill.id,
        name: skill.name,
        description: skill.description ?? null,
        ready: skill.ready,
        updatedAt: skill.lastSyncedAt ?? null
      })),
      files: trackedRootStates.filter((state): state is TrackedFileState => state !== null),
      memoryFiles: memoryStates
    });
    return createHash("sha1").update(statePayload).digest("hex");
  }

  private async buildSnapshot(buildState: ProjectContextBuildState): Promise<ProjectContextSnapshot> {
    const [soul, user, readme, memorySummary] = await Promise.all([
      this.readOptionalTextFile(path.join(buildState.projectRoot, "SOUL.md")),
      this.readOptionalTextFile(path.join(buildState.projectRoot, "USER.md")),
      this.readOptionalTextFile(path.join(buildState.projectRoot, "README.md")),
      this.buildMemorySummary(path.join(buildState.projectRoot, "memory"))
    ]);
    const boundSkills = buildState.skills.map((skill) => ({
      id: skill.id,
      name: skill.name,
      description: skill.description
    }));

    return {
      projectId: buildState.project.id,
      projectName: buildState.project.name,
      projectRoot: buildState.projectRoot,
      snapshotId: buildState.stateHash,
      generatedAt: new Date().toISOString(),
      soul,
      user,
      readme,
      memorySummary,
      boundSkills
    };
  }

  private renderSystemContext(snapshot: ProjectContextSnapshot): string {
    const sections: string[] = [
      "You are operating inside a desktop project-isolated workspace.",
      `Current project: ${snapshot.projectName} (${snapshot.projectId})`,
      `Project root: ${snapshot.projectRoot}`
    ];

    if (snapshot.boundSkills.length > 0) {
      sections.push([
        "Available project skills:",
        ...snapshot.boundSkills.map((skill) => `- ${skill.name} (${skill.id})${skill.description ? `: ${skill.description}` : ""}`)
      ].join("\n"));
    }

    if (snapshot.soul) {
      sections.push(["[SOUL.md]", snapshot.soul].join("\n"));
    }
    if (snapshot.user) {
      sections.push(["[USER.md]", snapshot.user].join("\n"));
    }
    if (snapshot.memorySummary) {
      sections.push(["[memory summary]", snapshot.memorySummary].join("\n"));
    }
    if (snapshot.readme) {
      sections.push(["[README.md]", snapshot.readme].join("\n"));
    }

    sections.push("Keep project context isolated to this project and do not mix state with other projects or sessions.");
    return sections.join("\n\n");
  }

  private async readOptionalTextFile(filePath: string): Promise<string | null> {
    if (!(await pathExists(filePath))) {
      return null;
    }
    const raw = await readFile(filePath, "utf8");
    const normalized = normalizeText(raw);
    return normalized || null;
  }

  private async buildMemorySummary(memoryRoot: string): Promise<string | null> {
    if (!(await pathExists(memoryRoot))) {
      return null;
    }

    const snippets = await this.collectMemorySnippets(memoryRoot);
    if (snippets.length === 0) {
      return null;
    }

    const summary = snippets.join("\n\n");
    return truncateText(summary, MAX_MEMORY_SUMMARY_LENGTH);
  }

  private async collectMemorySnippets(rootDir: string, currentDir = rootDir, bucket: string[] = []): Promise<string[]> {
    if (bucket.length >= MAX_MEMORY_FILES) {
      return bucket;
    }

    const entries = await readdir(currentDir, { withFileTypes: true }).catch(() => []);
    for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, "en"))) {
      if (bucket.length >= MAX_MEMORY_FILES) {
        break;
      }

      const targetPath = path.join(currentDir, entry.name);
      if (entry.isDirectory()) {
        await this.collectMemorySnippets(rootDir, targetPath, bucket);
        continue;
      }
      if (!entry.isFile() || !TEXT_FILE_PATTERN.test(entry.name)) {
        continue;
      }

      const raw = await readFile(targetPath, "utf8").catch(() => "");
      const normalized = normalizeText(raw);
      if (!normalized) {
        continue;
      }
      const relativePath = path.relative(rootDir, targetPath).replace(/\\/g, "/");
      bucket.push(`${relativePath}:\n${truncateText(normalized, MAX_MEMORY_SNIPPET_LENGTH)}`);
    }

    return bucket;
  }

  private async collectTrackedFileStates(rootDir: string, currentDir = rootDir, bucket: TrackedFileState[] = []): Promise<TrackedFileState[]> {
    if (!(await pathExists(currentDir)) || bucket.length >= MAX_TRACKED_MEMORY_FILES) {
      return bucket;
    }

    const entries = await readdir(currentDir, { withFileTypes: true }).catch(() => []);
    for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, "en"))) {
      if (bucket.length >= MAX_TRACKED_MEMORY_FILES) {
        break;
      }

      const targetPath = path.join(currentDir, entry.name);
      if (entry.isDirectory()) {
        await this.collectTrackedFileStates(rootDir, targetPath, bucket);
        continue;
      }
      if (!entry.isFile() || !TEXT_FILE_PATTERN.test(entry.name)) {
        continue;
      }

      const tracked = await this.readTrackedFileState(rootDir, targetPath);
      if (tracked) {
        bucket.push(tracked);
      }
    }

    return bucket;
  }

  private async readTrackedFileState(rootDir: string, filePath: string): Promise<TrackedFileState | null> {
    try {
      const fileStat = await stat(filePath);
      if (!fileStat.isFile()) {
        return null;
      }
      return {
        relativePath: path.relative(rootDir, filePath).replace(/\\/g, "/"),
        size: fileStat.size,
        mtimeMs: fileStat.mtimeMs
      };
    } catch {
      return null;
    }
  }
}
