import { eq, and, inArray } from "drizzle-orm";
import { rm } from "node:fs/promises";
import path from "node:path";
import { getSkillRegistryDb } from "./skill-registry-db.js";
import {
  skillGroups,
  skillGroupMembers,
  skillGroupInheritance,
  agentSkillGroups,
  agentSkills,
} from "./skill-registry-schema.js";
import { skillRegistryService } from "./skill-registry.js";

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

type GroupRow = typeof skillGroups.$inferSelect;
type MemberRow = typeof skillGroupMembers.$inferSelect;

type GroupExport = {
  version: "1";
  group: {
    id: string;
    name: string;
    description: string | null;
    members: string[];
    parents: string[];
  };
};

type AssignResult = { installed: string[]; skipped: string[]; pendingPlugin: string[] };

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

/**
 * Skill group service factory.
 * Manages its own libSQL database (does not accept a Postgres db param).
 * Use `getSkillRegistryDb()` for all persistence.
 */
export function skillGroupService() {
  // ---------------------------------------------------------------------------
  // Internal helpers
  // ---------------------------------------------------------------------------

  /**
   * BFS cycle detection: would adding parentGroupId as a parent of childGroupId
   * create a cycle? Returns true if childGroupId is reachable from parentGroupId
   * by walking up the inheritance chain.
   */
  async function wouldCreateCycle(
    childGroupId: string,
    parentGroupId: string,
  ): Promise<boolean> {
    const db = await getSkillRegistryDb();
    const visited = new Set<string>();
    const queue: string[] = [parentGroupId];

    while (queue.length > 0) {
      const current = queue.shift()!;
      if (current === childGroupId) return true;
      if (visited.has(current)) continue;
      visited.add(current);

      // Walk up: find parents of current
      const rows = await db
        .select()
        .from(skillGroupInheritance)
        .where(eq(skillGroupInheritance.childGroupId, current));
      for (const row of rows) {
        if (!visited.has(row.parentGroupId)) {
          queue.push(row.parentGroupId);
        }
      }
    }

    return false;
  }

  return {
    // -------------------------------------------------------------------------
    // Group CRUD
    // -------------------------------------------------------------------------

    async listGroups(): Promise<GroupRow[]> {
      const db = await getSkillRegistryDb();
      // Order: built-in first, then alphabetical by name
      const rows = await db.select().from(skillGroups);
      return rows.sort((a, b) => {
        if (a.isBuiltin !== b.isBuiltin) return b.isBuiltin - a.isBuiltin;
        return a.name.localeCompare(b.name);
      });
    },

    async getGroup(groupId: string): Promise<GroupRow | undefined> {
      const db = await getSkillRegistryDb();
      const rows = await db
        .select()
        .from(skillGroups)
        .where(eq(skillGroups.id, groupId));
      return rows[0];
    },

    async createGroup(input: {
      name: string;
      description?: string;
    }): Promise<GroupRow> {
      const db = await getSkillRegistryDb();
      const id = `custom/${input.name.toLowerCase().replace(/\s+/g, "-")}`;
      const now = Date.now();
      const row: typeof skillGroups.$inferInsert = {
        id,
        name: input.name,
        description: input.description ?? null,
        isBuiltin: 0,
        createdAt: now,
        updatedAt: now,
      };
      await db.insert(skillGroups).values(row);
      const inserted = await db
        .select()
        .from(skillGroups)
        .where(eq(skillGroups.id, id));
      return inserted[0]!;
    },

    async updateGroup(
      groupId: string,
      patch: { name?: string; description?: string },
    ): Promise<GroupRow> {
      const db = await getSkillRegistryDb();
      const existing = await this.getGroup(groupId);
      if (!existing) throw new Error("Group not found");

      const updates: Partial<typeof skillGroups.$inferInsert> = {
        updatedAt: Date.now(),
      };
      if (patch.name !== undefined) updates.name = patch.name;
      if (patch.description !== undefined) updates.description = patch.description;

      await db
        .update(skillGroups)
        .set(updates)
        .where(eq(skillGroups.id, groupId));

      const updated = await db
        .select()
        .from(skillGroups)
        .where(eq(skillGroups.id, groupId));
      return updated[0]!;
    },

    async deleteGroup(groupId: string): Promise<void> {
      const db = await getSkillRegistryDb();
      const existing = await this.getGroup(groupId);
      if (!existing) throw new Error("Group not found");
      if (existing.isBuiltin === 1)
        throw new Error("Cannot delete built-in group");

      // Remove all membership rows
      await db
        .delete(skillGroupMembers)
        .where(eq(skillGroupMembers.groupId, groupId));

      // Remove all inheritance rows (as parent or child)
      await db
        .delete(skillGroupInheritance)
        .where(eq(skillGroupInheritance.childGroupId, groupId));
      await db
        .delete(skillGroupInheritance)
        .where(eq(skillGroupInheritance.parentGroupId, groupId));

      // Remove all agent assignments
      await db
        .delete(agentSkillGroups)
        .where(eq(agentSkillGroups.groupId, groupId));

      // Remove group itself
      await db.delete(skillGroups).where(eq(skillGroups.id, groupId));
    },

    // -------------------------------------------------------------------------
    // Member management
    // -------------------------------------------------------------------------

    async addMember(groupId: string, skillId: string): Promise<void> {
      const db = await getSkillRegistryDb();
      await db
        .insert(skillGroupMembers)
        .values({ groupId, skillId, addedAt: Date.now() })
        .onConflictDoNothing();
    },

    async removeMember(groupId: string, skillId: string): Promise<void> {
      const db = await getSkillRegistryDb();
      await db
        .delete(skillGroupMembers)
        .where(
          and(
            eq(skillGroupMembers.groupId, groupId),
            eq(skillGroupMembers.skillId, skillId),
          ),
        );
    },

    async listMembers(groupId: string): Promise<MemberRow[]> {
      const db = await getSkillRegistryDb();
      return db
        .select()
        .from(skillGroupMembers)
        .where(eq(skillGroupMembers.groupId, groupId));
    },

    // -------------------------------------------------------------------------
    // Inheritance management
    // -------------------------------------------------------------------------

    async addParent(
      childGroupId: string,
      parentGroupId: string,
    ): Promise<void> {
      const db = await getSkillRegistryDb();
      const cycle = await wouldCreateCycle(childGroupId, parentGroupId);
      if (cycle)
        throw new Error("Adding this parent would create a cycle");

      await db
        .insert(skillGroupInheritance)
        .values({ childGroupId, parentGroupId })
        .onConflictDoNothing();
    },

    async removeParent(
      childGroupId: string,
      parentGroupId: string,
    ): Promise<void> {
      const db = await getSkillRegistryDb();
      await db
        .delete(skillGroupInheritance)
        .where(
          and(
            eq(skillGroupInheritance.childGroupId, childGroupId),
            eq(skillGroupInheritance.parentGroupId, parentGroupId),
          ),
        );
    },

    async listParents(groupId: string): Promise<string[]> {
      const db = await getSkillRegistryDb();
      const rows = await db
        .select()
        .from(skillGroupInheritance)
        .where(eq(skillGroupInheritance.childGroupId, groupId));
      return rows.map((r) => r.parentGroupId);
    },

    // -------------------------------------------------------------------------
    // Effective skill resolution
    // -------------------------------------------------------------------------

    /**
     * BFS walk through the group inheritance tree.
     * Collects all direct member skills from the group and all parent groups.
     * Uses a visited set to handle cycles safely.
     */
    async resolveEffectiveSkills(groupId: string): Promise<string[]> {
      const db = await getSkillRegistryDb();
      const visited = new Set<string>();
      const skillIds = new Set<string>();

      async function walk(gid: string): Promise<void> {
        if (visited.has(gid)) return;
        visited.add(gid);

        // Collect direct members
        const members = await db
          .select()
          .from(skillGroupMembers)
          .where(eq(skillGroupMembers.groupId, gid));
        for (const m of members) skillIds.add(m.skillId);

        // Recurse into parents
        const parents = await db
          .select()
          .from(skillGroupInheritance)
          .where(eq(skillGroupInheritance.childGroupId, gid));
        for (const p of parents) await walk(p.parentGroupId);
      }

      await walk(groupId);
      return Array.from(skillIds);
    },

    // -------------------------------------------------------------------------
    // Agent assignment
    // -------------------------------------------------------------------------

    async assignGroup(
      groupId: string,
      agentId: string,
      agentSkillsDir: string,
    ): Promise<AssignResult> {
      const db = await getSkillRegistryDb();
      const installed: string[] = [];
      const skipped: string[] = [];
      const pendingPlugin: string[] = [];

      // Idempotent assignment
      await db
        .insert(agentSkillGroups)
        .values({ agentId, groupId, assignedAt: Date.now() })
        .onConflictDoNothing();

      const skillIds = await this.resolveEffectiveSkills(groupId);
      const svc = skillRegistryService();

      for (const skillId of skillIds) {
        try {
          const result = await svc.install(skillId, agentSkillsDir);
          // Record in agent_skills
          await db
            .insert(agentSkills)
            .values({ agentId, skillId, installedAt: Date.now() })
            .onConflictDoNothing();

          if (result.type === "installed") {
            installed.push(skillId);
          } else {
            // pending_plugin_install
            pendingPlugin.push(result.command);
          }
        } catch (err) {
          // Don't block the entire assignment if one skill fails
          skipped.push(skillId);
        }
      }

      return { installed, skipped, pendingPlugin };
    },

    async removeGroup(
      groupId: string,
      agentId: string,
      agentSkillsDir: string,
    ): Promise<void> {
      const db = await getSkillRegistryDb();

      // Remove group assignment
      await db
        .delete(agentSkillGroups)
        .where(
          and(
            eq(agentSkillGroups.agentId, agentId),
            eq(agentSkillGroups.groupId, groupId),
          ),
        );

      // Get remaining groups
      const remainingRows = await db
        .select()
        .from(agentSkillGroups)
        .where(eq(agentSkillGroups.agentId, agentId));

      // Union all skills still required by remaining groups
      const stillNeeded = new Set<string>();
      for (const row of remainingRows) {
        const effective = await this.resolveEffectiveSkills(row.groupId);
        for (const sid of effective) stillNeeded.add(sid);
      }

      // Find skills that were contributed by the removed group
      const removedGroupSkills = await this.resolveEffectiveSkills(groupId);

      for (const skillId of removedGroupSkills) {
        // Skip if still needed by another remaining group
        if (stillNeeded.has(skillId)) continue;

        // Remove files from agent skills directory
        const slug = skillId.split("/").pop() ?? skillId;
        await rm(path.join(agentSkillsDir, slug), {
          recursive: true,
          force: true,
        });

        // Remove from agent_skills if present
        await db
          .delete(agentSkills)
          .where(
            and(
              eq(agentSkills.agentId, agentId),
              eq(agentSkills.skillId, skillId),
            ),
          );
      }
    },

    async listAgentGroups(agentId: string): Promise<GroupRow[]> {
      const db = await getSkillRegistryDb();
      const assignments = await db
        .select()
        .from(agentSkillGroups)
        .where(eq(agentSkillGroups.agentId, agentId));

      if (assignments.length === 0) return [];

      const groupIds = assignments.map((a) => a.groupId);
      return db
        .select()
        .from(skillGroups)
        .where(inArray(skillGroups.id, groupIds));
    },

    async listAgentSkills(agentId: string): Promise<Array<{ skillId: string; source: "managed" | "native"; installedAt: number }>> {
      const db = await getSkillRegistryDb();
      const rows = await db
        .select({
          skillId: agentSkills.skillId,
          source: agentSkills.source,
          installedAt: agentSkills.installedAt,
        })
        .from(agentSkills)
        .where(eq(agentSkills.agentId, agentId));
      return rows.map((r) => ({
        skillId: r.skillId,
        source: (r.source ?? "managed") as "managed" | "native",
        installedAt: r.installedAt,
      }));
    },

    async getAgentEffectiveSkills(agentId: string): Promise<string[]> {
      const groups = await this.listAgentGroups(agentId);
      const union = new Set<string>();
      for (const group of groups) {
        const skills = await this.resolveEffectiveSkills(group.id);
        for (const s of skills) union.add(s);
      }
      return Array.from(union);
    },

    // -------------------------------------------------------------------------
    // Import / Export
    // -------------------------------------------------------------------------

    async exportGroup(groupId: string): Promise<GroupExport> {
      const group = await this.getGroup(groupId);
      if (!group) throw new Error("Group not found");

      const memberRows = await this.listMembers(groupId);
      const parentIds = await this.listParents(groupId);

      return {
        version: "1",
        group: {
          id: group.id,
          name: group.name,
          description: group.description,
          members: memberRows.map((m) => m.skillId),
          parents: parentIds,
        },
      };
    },

    async importGroup(data: GroupExport): Promise<GroupRow> {
      if (data.version !== "1") throw new Error("Unsupported export version");

      const existing = await this.getGroup(data.group.id);
      if (existing) {
        throw new Error(
          `A group with id "${data.group.id}" already exists. Rename the group before importing.`,
        );
      }

      // Create the group
      const db = await getSkillRegistryDb();
      const now = Date.now();
      await db.insert(skillGroups).values({
        id: data.group.id,
        name: data.group.name,
        description: data.group.description,
        isBuiltin: 0,
        createdAt: now,
        updatedAt: now,
      });

      // Insert members
      for (const skillId of data.group.members) {
        await this.addMember(data.group.id, skillId);
      }

      // Insert parents (with cycle check via addParent)
      for (const parentId of data.group.parents) {
        await this.addParent(data.group.id, parentId);
      }

      const newGroup = await this.getGroup(data.group.id);
      return newGroup!;
    },
  };
}
