import { and, eq, ilike, inArray, or, ne } from "drizzle-orm";
import { getDb } from "../../db/client.js";
import { users, friendships } from "../../db/schema.js";
import { HttpError } from "../../middleware/errorHandler.js";
import { newId } from "../../utils/tokens.js";

export interface FriendUser {
  id: string;
  username: string | null;
  displayName: string;
  avatarUrl: string | null;
}

function toFriendUser(u: typeof users.$inferSelect): FriendUser {
  return {
    id: u.id,
    username: u.username,
    displayName: u.displayName,
    avatarUrl: u.avatarUrl,
  };
}

async function usersByIds(ids: string[]): Promise<Map<string, FriendUser>> {
  if (ids.length === 0) return new Map();
  const db = getDb();
  const rows = await db.select().from(users).where(inArray(users.id, ids));
  return new Map(rows.map((u) => [u.id, toFriendUser(u)]));
}

// All friendship rows involving `meId`, in either direction.
async function myFriendships(meId: string) {
  const db = getDb();
  return db
    .select()
    .from(friendships)
    .where(
      or(eq(friendships.requesterId, meId), eq(friendships.addresseeId, meId)),
    );
}

export async function listFriends(meId: string) {
  const rows = (await myFriendships(meId)).filter(
    (f) => f.status === "accepted",
  );
  const otherIds = rows.map((f) =>
    f.requesterId === meId ? f.addresseeId : f.requesterId,
  );
  const userMap = await usersByIds(otherIds);

  return rows
    .map((f) => {
      const otherId = f.requesterId === meId ? f.addresseeId : f.requesterId;
      const user = userMap.get(otherId);
      return user ? { friendshipId: f.id, user } : null;
    })
    .filter(Boolean);
}

export async function listRequests(meId: string) {
  const rows = (await myFriendships(meId)).filter(
    (f) => f.status === "pending",
  );
  const otherIds = rows.map((f) =>
    f.requesterId === meId ? f.addresseeId : f.requesterId,
  );
  const userMap = await usersByIds(otherIds);

  const incoming: any[] = [];
  const outgoing: any[] = [];
  for (const f of rows) {
    const otherId = f.requesterId === meId ? f.addresseeId : f.requesterId;
    const user = userMap.get(otherId);
    if (!user) continue;
    if (f.addresseeId === meId) incoming.push({ friendshipId: f.id, user });
    else outgoing.push({ friendshipId: f.id, user });
  }
  return { incoming, outgoing };
}

export async function searchUsers(meId: string, query: string) {
  const db = getDb();
  const q = query.trim();
  if (q.length < 2) return [];

  const found = await db
    .select()
    .from(users)
    .where(and(ilike(users.username, `${q}%`), ne(users.id, meId)))
    .limit(10);

  // Annotate each result with the current relationship (for button state).
  const rels = await myFriendships(meId);
  const relByOther = new Map<string, string>();
  for (const f of rels) {
    const otherId = f.requesterId === meId ? f.addresseeId : f.requesterId;
    const label =
      f.status === "accepted"
        ? "friends"
        : f.status === "blocked"
          ? "blocked"
          : f.requesterId === meId
            ? "pending_out"
            : "pending_in";
    relByOther.set(otherId, label);
  }

  return found.map((u) => ({
    ...toFriendUser(u),
    relationship: relByOther.get(u.id) ?? "none",
  }));
}

export async function sendRequest(meId: string, username: string) {
  const db = getDb();
  const [target] = await db
    .select()
    .from(users)
    .where(eq(users.username, username))
    .limit(1);

  if (!target) throw new HttpError(404, "user_not_found");
  if (target.id === meId) throw new HttpError(400, "cannot_add_self");

  const [existing] = await db
    .select()
    .from(friendships)
    .where(
      or(
        and(
          eq(friendships.requesterId, meId),
          eq(friendships.addresseeId, target.id),
        ),
        and(
          eq(friendships.requesterId, target.id),
          eq(friendships.addresseeId, meId),
        ),
      ),
    )
    .limit(1);

  if (existing) {
    if (existing.status === "accepted")
      throw new HttpError(409, "already_friends");
    if (existing.status === "blocked") throw new HttpError(403, "blocked");
    throw new HttpError(409, "request_already_exists");
  }

  const now = Date.now();
  const [row] = await db
    .insert(friendships)
    .values({
      id: newId(),
      requesterId: meId,
      addresseeId: target.id,
      status: "pending",
      createdAt: now,
      updatedAt: now,
    })
    .returning();

  return { friendshipId: row.id, user: toFriendUser(target) };
}

export async function acceptRequest(meId: string, friendshipId: string) {
  const db = getDb();
  const [f] = await db
    .select()
    .from(friendships)
    .where(eq(friendships.id, friendshipId))
    .limit(1);
  if (!f || f.addresseeId !== meId || f.status !== "pending") {
    throw new HttpError(404, "request_not_found");
  }
  await db
    .update(friendships)
    .set({ status: "accepted", updatedAt: Date.now() })
    .where(eq(friendships.id, friendshipId));
}

export async function declineRequest(meId: string, friendshipId: string) {
  const db = getDb();
  const [f] = await db
    .select()
    .from(friendships)
    .where(eq(friendships.id, friendshipId))
    .limit(1);
  if (!f || f.addresseeId !== meId || f.status !== "pending") {
    throw new HttpError(404, "request_not_found");
  }
  await db.delete(friendships).where(eq(friendships.id, friendshipId));
}

export async function removeFriend(meId: string, friendshipId: string) {
  const db = getDb();
  const [f] = await db
    .select()
    .from(friendships)
    .where(eq(friendships.id, friendshipId))
    .limit(1);
  if (!f || (f.requesterId !== meId && f.addresseeId !== meId)) {
    throw new HttpError(404, "friendship_not_found");
  }
  await db.delete(friendships).where(eq(friendships.id, friendshipId));
}

export async function blockFriend(meId: string, friendshipId: string) {
  const db = getDb();
  const [f] = await db
    .select()
    .from(friendships)
    .where(eq(friendships.id, friendshipId))
    .limit(1);
  if (!f || (f.requesterId !== meId && f.addresseeId !== meId)) {
    throw new HttpError(404, "friendship_not_found");
  }
  await db
    .update(friendships)
    .set({ status: "blocked", updatedAt: Date.now() })
    .where(eq(friendships.id, friendshipId));
}
