import { execFile } from "node:child_process";
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

const GITHUB_API = "https://api.github.com";
const DEFAULT_HEADERS = {
  accept: "application/vnd.github+json",
  "user-agent": "hermes-vinod-github-owners/1.0",
  "x-github-api-version": "2022-11-28"
};

export class GitHubAdapter {
  constructor({ token = process.env.GITHUB_TOKEN, rootDir } = {}) {
    this.token = token || "";
    this.rootDir = rootDir;
    this.configPath = path.join(rootDir || process.cwd(), "data", "repos", "platform-repos.json");
  }

  configured() {
    return Boolean(this.configPath && existsSync(this.configPath));
  }

  loadPlatforms() {
    if (!existsSync(this.configPath)) {
      return { platforms: {}, platformOrder: [] };
    }
    const raw = JSON.parse(readFileSync(this.configPath, "utf8"));
    const platforms = raw.platforms || {};
    return {
      platforms,
      platformOrder: Object.keys(platforms),
      raw
    };
  }

  headers() {
    return this.token
      ? { ...DEFAULT_HEADERS, authorization: `Bearer ${this.token}` }
      : { ...DEFAULT_HEADERS };
  }

  async searchRepos({ platform, platformLabel, repos, perRepo = 1 }) {
    if (!repos?.length) return [];
    const out = [];
    for (const fullName of repos) {
      try {
        const meta = await this.fetchRepo(fullName);
        out.push({ platform, platformLabel, repo: meta });
      } catch (error) {
        out.push({
          platform,
          platformLabel,
          repo: { full_name: fullName, error: error.message }
        });
      }
    }
    return out;
  }

  async fetchRepo(fullName) {
    const response = await fetch(`${GITHUB_API}/repos/${fullName}`, { headers: this.headers() });
    if (!response.ok) {
      throw new Error(`GitHub repo ${fullName} HTTP ${response.status}`);
    }
    return response.json();
  }

  async fetchOwner(login) {
    if (!login) return null;
    const response = await fetch(`${GITHUB_API}/users/${login}`, { headers: this.headers() });
    if (!response.ok) return null;
    const user = await response.json();
    return user;
  }

  async fetchCommitEmails({ fullName, limit = 12 }) {
    if (!fullName) return [];
    try {
      const response = await fetch(
        `${GITHUB_API}/repos/${fullName}/commits?per_page=${limit}`,
        { headers: this.headers() }
      );
      if (!response.ok) return [];
      const commits = await response.json();
      const emails = new Set();
      for (const commit of commits) {
        const author = commit?.author?.email;
        const commitAuthor = commit?.commit?.author?.email;
        const commitCommitter = commit?.commit?.committer?.email;
        for (const email of [author, commitAuthor, commitCommitter]) {
          if (!email) continue;
          if (email.endsWith("@users.noreply.github.com")) continue;
          if (email.includes("noreply")) continue;
          emails.add(email.toLowerCase());
        }
      }
      return [...emails];
    } catch {
      return [];
    }
  }

  async fetchPublicEvents(login, { perPage = 30 } = {}) {
    if (!login) return [];
    try {
      const response = await fetch(
        `${GITHUB_API}/users/${login}/events/public?per_page=${perPage}`,
        { headers: this.headers() }
      );
      if (!response.ok) return [];
      return await response.json();
    } catch {
      return [];
    }
  }

  async cloneForCommitLog(fullName) {
    if (!this.rootDir) return null;
    const cacheBase = path.join(this.rootDir, "data", "raw", "github-clones");
    mkdirSync(cacheBase, { recursive: true });
    const dir = await mkdtemp(path.join(cacheBase, `${safeFolder(fullName)}-`));
    try {
      await execFileAsync("git", [
        "clone",
        "--depth=50",
          "--filter=blob:none",
          "--no-checkout",
          `https://github.com/${fullName}.git`,
          dir
        ],
        { timeout: 60_000, maxBuffer: 8 * 1024 * 1024 }
      );
      return dir;
    } catch (error) {
      try { await rm(dir, { recursive: true, force: true }); } catch {}
      return null;
    }
  }

  async extractCommitEmailsFromClone(fullName) {
    const dir = await this.cloneForCommitLog(fullName);
    if (!dir) return [];
    try {
      const { stdout } = await execFileAsync("git", [
        "-C", dir, "log", "-50", "--pretty=format:%ae|%an"
      ], { maxBuffer: 4 * 1024 * 1024 });
      const emails = new Set();
      for (const line of stdout.split("\n")) {
        const [email] = line.split("|");
        if (!email) continue;
        const lower = email.toLowerCase();
        if (lower.endsWith("@users.noreply.github.com")) continue;
        if (lower.includes("noreply")) continue;
        emails.add(lower);
      }
      return [...emails];
    } catch {
      return [];
    } finally {
      try { await rm(dir, { recursive: true, force: true }); } catch {}
    }
  }

  async gatherOwnerBundle({ repo, platform, platformLabel }) {
    const login = repo?.owner?.login;
    const owner = await this.fetchOwner(login);
    const apiEmails = await this.fetchCommitEmails({ fullName: repo.full_name });
    const clonedEmails = apiEmails.length
      ? []
      : await this.extractCommitEmailsFromClone(repo.full_name);
    return {
      platform,
      platformLabel,
      repo: {
        fullName: repo.full_name,
        description: repo.description || "",
        htmlUrl: repo.html_url,
        stars: repo.stargazers_count,
        forks: repo.forks_count,
        language: repo.language,
        license: repo.license?.spdx_id || "",
        pushedAt: repo.pushed_at,
        updatedAt: repo.updated_at,
        archived: Boolean(repo.archived),
        topics: repo.topics || [],
        homepage: repo.homepage || ""
      },
      owner: owner && {
        login: owner.login,
        name: owner.name || "",
        bio: owner.bio || "",
        company: owner.company || "",
        blog: owner.blog || "",
        website: normalizeUrl(owner.blog),
        twitter: owner.twitter_username || "",
        location: owner.location || "",
        email: owner.email || "",
        htmlUrl: owner.html_url,
        avatarUrl: owner.avatar_url,
        publicRepos: owner.public_repos,
        followers: owner.followers,
        following: owner.following,
        createdAt: owner.created_at,
        hireable: Boolean(owner.hireable)
      },
      emails: uniqueList([...(owner?.email ? [owner.email] : []), ...apiEmails, ...clonedEmails])
    };
  }
}

function safeFolder(value) {
  return String(value || "").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
}

function uniqueList(items) {
  const seen = new Set();
  const out = [];
  for (const raw of items) {
    const value = String(raw || "").trim().toLowerCase();
    if (!value || seen.has(value)) continue;
    seen.add(value);
    out.push(value);
  }
  return out;
}

function normalizeUrl(value) {
  const text = String(value || "").trim();
  if (!text) return "";
  if (/^https?:\/\//i.test(text)) return text;
  return `https://${text}`;
}
