import { eq, isNull, and, desc, sql } from "drizzle-orm";
import { cp, mkdir, rm, readdir } from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { getSkillRegistryDb } from "./skill-registry-db.js";
import { skills, skillVersions, skillFiles, communityRatings, agentSkills } from "./skill-registry-schema.js";
import { fetchAllSources, type SkillSourceConfig } from "./skill-registry-fetcher.js";
import { resolveSkillCacheDir } from "../home-paths.js";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

type SkillRow = typeof skills.$inferSelect;
type VersionRow = typeof skillVersions.$inferSelect;

/** Extended skill list item with community rating and usage stats from JOINs */
type SkillListItem = SkillRow & {
  averageRating: number | null;
  ratingCount: number | null;
  taskCount: number | null;
  avgCostUsd: number | null;
  lastUsedAt: number | null;
};

type InstallResult =
  | { type: "installed"; skillId: string; versionId: string; targetDir: string }
  | { type: "pending_plugin_install"; command: string; skillId: string; versionId: string };

// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------

/**
 * Skill registry service factory.
 * Manages its own libSQL database (does not accept a Postgres db param).
 * Use `getSkillRegistryDb()` for all persistence.
 */
export function skillRegistryService() {
  return {
    async list(opts?: { includeRemoved?: boolean }): Promise<SkillListItem[]> {
      const db = await getSkillRegistryDb();

      const query = db
        .select({
          // All skills columns
          id: skills.id,
          sourceId: skills.sourceId,
          name: skills.name,
          description: skills.description,
          sourceUrl: skills.sourceUrl,
          activeVersionId: skills.activeVersionId,
          removedAt: skills.removedAt,
          createdAt: skills.createdAt,
          updatedAt: skills.updatedAt,
          // Community rating fields from LEFT JOIN
          averageRating: communityRatings.averageRating,
          ratingCount: communityRatings.ratingCount,
          // Aggregated usage stats across all agents
          taskCount: sql<number | null>`SUM(${agentSkills.taskCount})`,
          avgCostUsd: sql<number | null>`AVG(${agentSkills.avgCostUsd})`,
          lastUsedAt: sql<number | null>`MAX(${agentSkills.lastUsedAt})`,
        })
        .from(skills)
        .leftJoin(communityRatings, eq(communityRatings.skillId, skills.id))
        .leftJoin(agentSkills, eq(agentSkills.skillId, skills.id))
        .groupBy(skills.id, communityRatings.id);

      if (opts?.includeRemoved) {
        return query as Promise<SkillListItem[]>;
      }
      return query.where(isNull(skills.removedAt)) as Promise<SkillListItem[]>;
    },

    async getById(skillId: string, opts?: { includeRemoved?: boolean }): Promise<SkillListItem | undefined> {
      const db = await getSkillRegistryDb();

      const conditions: Parameters<typeof and>[0][] = [eq(skills.id, skillId)];
      if (!opts?.includeRemoved) conditions.push(isNull(skills.removedAt));

      const rows = await db
        .select({
          id: skills.id,
          sourceId: skills.sourceId,
          name: skills.name,
          description: skills.description,
          sourceUrl: skills.sourceUrl,
          activeVersionId: skills.activeVersionId,
          removedAt: skills.removedAt,
          createdAt: skills.createdAt,
          updatedAt: skills.updatedAt,
          averageRating: communityRatings.averageRating,
          ratingCount: communityRatings.ratingCount,
          taskCount: sql<number | null>`SUM(${agentSkills.taskCount})`,
          avgCostUsd: sql<number | null>`AVG(${agentSkills.avgCostUsd})`,
          lastUsedAt: sql<number | null>`MAX(${agentSkills.lastUsedAt})`,
        })
        .from(skills)
        .leftJoin(communityRatings, eq(communityRatings.skillId, skills.id))
        .leftJoin(agentSkills, eq(agentSkills.skillId, skills.id))
        .groupBy(skills.id, communityRatings.id)
        .where(and(...conditions));
      return rows[0] as SkillListItem | undefined;
    },

    async getVersions(skillId: string): Promise<VersionRow[]> {
      const db = await getSkillRegistryDb();
      return db.select().from(skillVersions).where(eq(skillVersions.skillId, skillId));
    },

    async install(skillId: string, agentSkillsDir: string): Promise<InstallResult> {
      const db = await getSkillRegistryDb();
      const skill = await this.getById(skillId);
      if (!skill) throw new Error(`Skill not found: ${skillId}`);

      // Get latest version (most recently fetched)
      const versions = await db
        .select()
        .from(skillVersions)
        .where(eq(skillVersions.skillId, skillId))
        .orderBy(desc(skillVersions.fetchedAt));
      const latest = versions[0];
      if (!latest) throw new Error(`No versions found for skill: ${skillId}`);

      // Check if this is a marketplace plugin — identified by any file having kind="plugin"
      const files = await db
        .select()
        .from(skillFiles)
        .where(eq(skillFiles.versionId, latest.id));
      const isPlugin = files.some((f) => f.kind === "plugin");

      if (isPlugin) {
        // Return pending plugin install command instead of copying files
        const slug = skillId.split("/").pop() ?? skillId;
        return {
          type: "pending_plugin_install" as const,
          command: `/plugin install ${slug}@marketplace`,
          skillId,
          versionId: latest.id,
        };
      }

      // Copy cached files to agent skills dir
      const cacheDir = latest.cacheDir ?? resolveSkillCacheDir(skillId, latest.id);
      const slug = skillId.split("/").pop() ?? skillId;
      const targetDir = path.join(agentSkillsDir, slug);
      await mkdir(targetDir, { recursive: true });
      await cp(cacheDir, targetDir, { recursive: true });

      // Update active version
      await db
        .update(skills)
        .set({ activeVersionId: latest.id, updatedAt: Date.now() })
        .where(eq(skills.id, skillId));

      return {
        type: "installed" as const,
        skillId,
        versionId: latest.id,
        targetDir,
      };
    },

    async uninstall(skillId: string, agentSkillsDir: string): Promise<void> {
      const db = await getSkillRegistryDb();

      // Remove skill files from disk before soft-deleting the registry row
      const slug = skillId.split("/").pop() ?? skillId;
      const targetDir = path.join(agentSkillsDir, slug);
      await rm(targetDir, { recursive: true, force: true });

      await db
        .update(skills)
        .set({ removedAt: Date.now(), updatedAt: Date.now() })
        .where(eq(skills.id, skillId));
    },

    async rollback(skillId: string, versionId: string, agentSkillsDir: string): Promise<void> {
      const db = await getSkillRegistryDb();
      const versionRows = await db
        .select()
        .from(skillVersions)
        .where(eq(skillVersions.id, versionId));
      const version = versionRows[0];
      if (!version) throw new Error(`Version not found: ${versionId}`);

      const cacheDir = version.cacheDir ?? resolveSkillCacheDir(skillId, versionId);
      const slug = skillId.split("/").pop() ?? skillId;
      const targetDir = path.join(agentSkillsDir, slug);

      // Remove current files, restore from cache
      await rm(targetDir, { recursive: true, force: true });
      await mkdir(targetDir, { recursive: true });
      await cp(cacheDir, targetDir, { recursive: true });

      // Update active version to the rolled-back version
      await db
        .update(skills)
        .set({ activeVersionId: versionId, updatedAt: Date.now() })
        .where(eq(skills.id, skillId));
    },

    /**
     * Sync native Hermes skills from ~/.hermes/skills/ into the registry.
     * Creates a minimal skills stub row and an agentSkills row with source='native'
     * for each subdirectory found. Idempotent — safe to call multiple times.
     * Returns silently if ~/.hermes/skills/ does not exist.
     */
    async syncHermesNativeSkills(agentId: string): Promise<void> {
      const hermesSkillsDir = path.join(os.homedir(), ".hermes", "skills");
      let entries: string[];
      try {
        entries = await readdir(hermesSkillsDir);
      } catch {
        // Directory doesn't exist — no native skills
        return;
      }

      const db = await getSkillRegistryDb();
      const now = Date.now();

      for (const entry of entries) {
        const skillId = `hermes-native/${entry}`;

        // Create a minimal stub in the skills table so JOINs work
        await db.insert(skills).values({
          id: skillId,
          sourceId: "hermes-native",
          name: entry,
          description: null,
          sourceUrl: null,
          activeVersionId: null,
          removedAt: null,
          createdAt: now,
          updatedAt: now,
        }).onConflictDoNothing();

        // Record in agent_skills with source = 'native'
        await db.insert(agentSkills).values({
          agentId,
          skillId,
          installedAt: now,
          source: "native",
        }).onConflictDoNothing();
      }
    },

    async fetchAll(
      sources?: SkillSourceConfig[],
    ): Promise<{ fetched: number; errors: string[] }> {
      return fetchAllSources(sources);
    },
  };
}
