// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

/**
 * NERF Skill Marketplace — publish, discover, and install NERF skills.
 *
 * SQLite-backed CRUD with ratings/reviews, search, and JSON-based
 * export/import (simplified from Python's tar.gz to reduce complexity).
 */

import { randomUUID } from 'node:crypto';
import { createHash } from 'node:crypto';
import { homedir } from 'node:os';
import { join, dirname, resolve as resolvePath } from 'node:path';
import { mkdirSync, existsSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync, rmdirSync } from 'node:fs';
import { createGzip, createGunzip } from 'node:zlib';

/** @type {typeof import('better-sqlite3')} */
let Database;

/**
 * Compute SHA-256 checksum over all file contents sorted by name.
 * @param {Record<string, string>} files
 * @returns {string}
 */
function computeChecksum(files) {
  const h = createHash('sha256');
  for (const name of Object.keys(files).sort()) {
    h.update(name, 'utf8');
    h.update(files[name], 'utf8');
  }
  return h.digest('hex');
}

/**
 * Skill marketplace for publishing, discovering, and installing NERF skills.
 */
export class SkillMarketplace {
  /**
   * @param {object} [opts]
   * @param {string} [opts.dbPath] - Path to SQLite DB. Default: ~/.nerf/marketplace.db
   * @param {string} [opts.skillsDir] - Default install directory for skills. Default: 'skills'
   */
  constructor(opts = {}) {
    if (!Database) {
      Database = require('better-sqlite3');
    }
    const dbPath = opts.dbPath || join(homedir(), '.nerf', 'marketplace.db');
    this.skillsDir = opts.skillsDir || 'skills';
    mkdirSync(dirname(dbPath), { recursive: true });
    this._db = new Database(dbPath);
    this._db.pragma('journal_mode = WAL');
    this._initDb();
  }

  _initDb() {
    this._db.exec(`
      CREATE TABLE IF NOT EXISTS packages (
        package_id   TEXT PRIMARY KEY,
        name         TEXT NOT NULL,
        domain       TEXT NOT NULL DEFAULT '',
        version      TEXT NOT NULL DEFAULT '0.1.0',
        description  TEXT NOT NULL DEFAULT '',
        author       TEXT NOT NULL DEFAULT '',
        license      TEXT NOT NULL DEFAULT 'AGPL-3.0',
        tags         TEXT NOT NULL DEFAULT '[]',
        files        TEXT NOT NULL DEFAULT '{}',
        checksum     TEXT NOT NULL DEFAULT '',
        downloads    INTEGER NOT NULL DEFAULT 0,
        rating       REAL NOT NULL DEFAULT 0.0,
        ratings_count INTEGER NOT NULL DEFAULT 0,
        created_at   TEXT NOT NULL,
        updated_at   TEXT NOT NULL
      );

      CREATE TABLE IF NOT EXISTS reviews (
        review_id   TEXT PRIMARY KEY,
        package_id  TEXT NOT NULL,
        reviewer    TEXT NOT NULL,
        rating      REAL NOT NULL,
        comment     TEXT NOT NULL DEFAULT '',
        created_at  TEXT NOT NULL,
        FOREIGN KEY (package_id) REFERENCES packages(package_id)
      );

      CREATE TABLE IF NOT EXISTS download_log (
        id          INTEGER PRIMARY KEY AUTOINCREMENT,
        package_id  TEXT NOT NULL,
        downloaded_at TEXT NOT NULL,
        target_dir  TEXT NOT NULL DEFAULT '',
        FOREIGN KEY (package_id) REFERENCES packages(package_id)
      );

      CREATE INDEX IF NOT EXISTS idx_packages_domain ON packages(domain);
      CREATE INDEX IF NOT EXISTS idx_packages_rating ON packages(rating DESC);
      CREATE INDEX IF NOT EXISTS idx_reviews_package ON reviews(package_id);
    `);
  }

  // -- Publish / get / list / search ----------------------------------------

  /**
   * Publish a skill package to the marketplace. Returns package_id.
   * @param {object} pkg
   * @returns {string} package_id
   */
  publish(pkg) {
    const packageId = pkg.packageId || randomUUID();
    const now = new Date().toISOString();
    const files = pkg.files || {};
    const checksum = pkg.checksum || (Object.keys(files).length ? computeChecksum(files) : '');
    const createdAt = pkg.createdAt || now;

    this._db
      .prepare(
        `INSERT INTO packages (
          package_id, name, domain, version, description, author,
          license, tags, files, checksum, downloads, rating,
          ratings_count, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(package_id) DO UPDATE SET
          name=excluded.name, domain=excluded.domain,
          version=excluded.version, description=excluded.description,
          author=excluded.author, license=excluded.license,
          tags=excluded.tags, files=excluded.files,
          checksum=excluded.checksum, updated_at=excluded.updated_at`,
      )
      .run(
        packageId,
        pkg.name || '',
        pkg.domain || '',
        pkg.version || '0.1.0',
        pkg.description || '',
        pkg.author || '',
        pkg.license || 'AGPL-3.0',
        JSON.stringify(pkg.tags || []),
        JSON.stringify(files),
        checksum,
        pkg.downloads || 0,
        pkg.rating || 0,
        pkg.ratingsCount || 0,
        createdAt,
        now,
      );
    return packageId;
  }

  /**
   * Retrieve a single package by ID.
   * @param {string} packageId
   * @returns {object|null}
   */
  getPackage(packageId) {
    const row = this._db.prepare('SELECT * FROM packages WHERE package_id = ?').get(packageId);
    return row ? this._rowToPackage(row) : null;
  }

  /**
   * List packages with optional domain filter and sorting.
   * @param {object} [opts]
   * @param {string} [opts.domain]
   * @param {string} [opts.sort='rating'] - 'rating'|'downloads'|'name'|'updated'|'created'
   * @param {number} [opts.limit=20]
   * @returns {object[]}
   */
  listPackages(opts = {}) {
    const sortMap = {
      rating: 'rating DESC',
      downloads: 'downloads DESC',
      name: 'name ASC',
      updated: 'updated_at DESC',
      created: 'created_at DESC',
    };
    const order = sortMap[opts.sort || 'rating'] || 'rating DESC';
    const limit = opts.limit || 20;

    let rows;
    if (opts.domain) {
      rows = this._db
        .prepare(`SELECT * FROM packages WHERE domain = ? ORDER BY ${order} LIMIT ?`)
        .all(opts.domain, limit);
    } else {
      rows = this._db
        .prepare(`SELECT * FROM packages ORDER BY ${order} LIMIT ?`)
        .all(limit);
    }
    return rows.map((r) => this._rowToPackage(r));
  }

  /**
   * Search packages by name, description, and optional filters.
   * @param {string} query
   * @param {object} [opts]
   * @param {string} [opts.domain]
   * @param {string[]} [opts.tags]
   * @returns {object[]}
   */
  search(query, opts = {}) {
    const conditions = ['(name LIKE ? OR description LIKE ? OR tags LIKE ?)'];
    const params = [`%${query}%`, `%${query}%`, `%${query}%`];

    if (opts.domain) {
      conditions.push('domain = ?');
      params.push(opts.domain);
    }
    if (opts.tags) {
      for (const tag of opts.tags) {
        conditions.push('tags LIKE ?');
        params.push(`%"${tag}"%`);
      }
    }

    const where = conditions.join(' AND ');
    const rows = this._db
      .prepare(`SELECT * FROM packages WHERE ${where} ORDER BY rating DESC`)
      .all(...params);
    return rows.map((r) => this._rowToPackage(r));
  }

  // -- Install / uninstall --------------------------------------------------

  /**
   * Install a skill package to the target directory.
   * @param {string} packageId
   * @param {string} [targetDir]
   * @returns {boolean}
   */
  install(packageId, targetDir) {
    const pkg = this.getPackage(packageId);
    if (!pkg) return false;

    const dest = resolvePath(targetDir || join(this.skillsDir, pkg.name));
    mkdirSync(dest, { recursive: true });

    for (const [filename, content] of Object.entries(pkg.files)) {
      const filepath = resolvePath(dest, filename);
      // Prevent path traversal
      if (!filepath.startsWith(dest)) continue;
      mkdirSync(dirname(filepath), { recursive: true });
      writeFileSync(filepath, content, 'utf8');
    }

    // Record download
    const now = new Date().toISOString();
    this._db.prepare('UPDATE packages SET downloads = downloads + 1 WHERE package_id = ?').run(packageId);
    this._db
      .prepare('INSERT INTO download_log (package_id, downloaded_at, target_dir) VALUES (?, ?, ?)')
      .run(packageId, now, dest);
    return true;
  }

  /**
   * Remove installed skill files from disk.
   * @param {string} packageId
   * @returns {boolean}
   */
  uninstall(packageId) {
    const pkg = this.getPackage(packageId);
    if (!pkg) return false;

    const possibleDirs = [resolvePath(this.skillsDir, pkg.name)];

    // Check download_log
    const row = this._db
      .prepare('SELECT target_dir FROM download_log WHERE package_id = ? ORDER BY downloaded_at DESC LIMIT 1')
      .get(packageId);
    if (row?.target_dir) {
      possibleDirs.unshift(row.target_dir);
    }

    let removed = false;
    for (const dir of possibleDirs) {
      if (!existsSync(dir)) continue;
      for (const filename of Object.keys(pkg.files)) {
        const fpath = join(dir, filename);
        if (existsSync(fpath)) {
          try { unlinkSync(fpath); } catch { /* skip */ }
        }
      }
      // Try to remove empty directory
      try {
        const remaining = readdirSync(dir);
        if (remaining.length === 0) rmdirSync(dir);
      } catch { /* directory not empty or doesn't exist */ }
      removed = true;
    }
    return removed;
  }

  // -- Ratings / reviews ----------------------------------------------------

  /**
   * Add a rating/review for a package.
   * @param {string} packageId
   * @param {string} reviewer
   * @param {number} rating - 1.0 to 5.0
   * @param {string} [comment='']
   * @returns {boolean}
   */
  rate(packageId, reviewer, rating, comment = '') {
    if (!this.getPackage(packageId)) return false;

    const clampedRating = Math.max(1.0, Math.min(5.0, rating));
    const reviewId = randomUUID();
    const now = new Date().toISOString();

    this._db
      .prepare(
        'INSERT INTO reviews (review_id, package_id, reviewer, rating, comment, created_at) VALUES (?, ?, ?, ?, ?, ?)',
      )
      .run(reviewId, packageId, reviewer, clampedRating, comment, now);

    // Recompute average rating
    const stats = this._db
      .prepare('SELECT AVG(rating) as avg_r, COUNT(*) as cnt FROM reviews WHERE package_id = ?')
      .get(packageId);
    this._db
      .prepare('UPDATE packages SET rating = ?, ratings_count = ? WHERE package_id = ?')
      .run(Math.round(stats.avg_r * 100) / 100, stats.cnt, packageId);

    return true;
  }

  /**
   * Get all reviews for a package.
   * @param {string} packageId
   * @returns {object[]}
   */
  getReviews(packageId) {
    return this._db
      .prepare('SELECT * FROM reviews WHERE package_id = ? ORDER BY created_at DESC')
      .all(packageId)
      .map((r) => ({
        reviewId: r.review_id,
        packageId: r.package_id,
        reviewer: r.reviewer,
        rating: r.rating,
        comment: r.comment,
        createdAt: r.created_at,
      }));
  }

  // -- Marketplace index ----------------------------------------------------

  /**
   * Build a summary index of the marketplace.
   * @returns {object}
   */
  getIndex() {
    const totalPackages = this._db.prepare('SELECT COUNT(*) as cnt FROM packages').get().cnt;
    const totalDownloads = this._db.prepare('SELECT COALESCE(SUM(downloads), 0) as total FROM packages').get().total;

    const catRows = this._db.prepare('SELECT domain, COUNT(*) as cnt FROM packages GROUP BY domain').all();
    const categories = {};
    for (const r of catRows) categories[r.domain] = r.cnt;

    const topRated = this._db
      .prepare('SELECT package_id, name, domain, rating, downloads FROM packages ORDER BY rating DESC LIMIT 10')
      .all();
    const mostDownloaded = this._db
      .prepare('SELECT package_id, name, domain, rating, downloads FROM packages ORDER BY downloads DESC LIMIT 10')
      .all();
    const recentlyUpdated = this._db
      .prepare('SELECT package_id, name, domain, version, updated_at FROM packages ORDER BY updated_at DESC LIMIT 10')
      .all();

    return {
      totalPackages,
      totalDownloads,
      categories,
      topRated,
      mostDownloaded,
      recentlyUpdated,
    };
  }

  // -- Export / import as JSON archive ----------------------------------------

  /**
   * Export a package as a JSON archive string.
   * @param {string} packageId
   * @returns {string} JSON string
   */
  exportPackage(packageId) {
    const pkg = this.getPackage(packageId);
    if (!pkg) throw new Error(`Package not found: ${packageId}`);
    return JSON.stringify(
      {
        manifest: {
          package_id: pkg.packageId,
          name: pkg.name,
          domain: pkg.domain,
          version: pkg.version,
          description: pkg.description,
          author: pkg.author,
          license: pkg.license,
          tags: pkg.tags,
          checksum: pkg.checksum,
          created_at: pkg.createdAt,
          updated_at: pkg.updatedAt,
        },
        files: pkg.files,
      },
      null,
      2,
    );
  }

  /**
   * Import a package from a JSON archive string.
   * @param {string} jsonStr
   * @returns {object} The imported package object
   */
  importPackage(jsonStr) {
    const data = JSON.parse(jsonStr);
    const manifest = data.manifest || {};
    const files = data.files || {};

    // Verify checksum
    const computed = computeChecksum(files);
    if (manifest.checksum && manifest.checksum !== computed) {
      throw new Error(`Checksum mismatch: expected ${manifest.checksum}, got ${computed}`);
    }

    return {
      packageId: manifest.package_id || randomUUID(),
      name: manifest.name || '',
      domain: manifest.domain || '',
      version: manifest.version || '0.1.0',
      description: manifest.description || '',
      author: manifest.author || '',
      license: manifest.license || 'AGPL-3.0',
      tags: manifest.tags || [],
      files,
      checksum: computed,
      createdAt: manifest.created_at || '',
      updatedAt: manifest.updated_at || '',
    };
  }

  // -- Helpers ---------------------------------------------------------------

  _rowToPackage(row) {
    return {
      packageId: row.package_id,
      name: row.name,
      domain: row.domain,
      version: row.version,
      description: row.description,
      author: row.author,
      license: row.license,
      tags: JSON.parse(row.tags),
      files: JSON.parse(row.files),
      checksum: row.checksum,
      downloads: row.downloads,
      rating: row.rating,
      ratingsCount: row.ratings_count,
      createdAt: row.created_at,
      updatedAt: row.updated_at,
    };
  }

  /** Close the database connection. */
  close() {
    this._db.close();
  }
}
