import type { FastifyInstance } from 'fastify';
import { eq, and, sql, inArray } from 'drizzle-orm';
import { getDb, getRawDb, schema } from '../db/index.js';
import { authenticate } from '../utils/auth.js';
import { generateSnowflake } from '../utils/snowflake.js';
import { isMember, isBanned, isSpaceOwner, hasPermission, computePermissions, PermissionBits, permissionsToString } from '../utils/permissions.js';
import { connectionManager } from '../ws/handler.js';
import { sanitizeUser } from '../utils/sanitize.js';
import type {
  ExploreSpace,
  JoinRequest,
  MemberWithUser,
  SpaceWithChannelsAndMembers,
  Channel,
} from '@backspace/shared';

function rowToJoinRequest(
  row: typeof schema.joinRequests.$inferSelect,
  user?: ReturnType<typeof sanitizeUser>,
): JoinRequest {
  return {
    id: row.id,
    spaceId: row.spaceId,
    userId: row.userId,
    message: row.message,
    status: row.status as JoinRequest['status'],
    decidedBy: row.decidedBy,
    createdAt: row.createdAt,
    decidedAt: row.decidedAt,
    user: user ?? undefined,
  };
}

/**
 * Find all online users for a space who have MANAGE_SPACE or are the owner.
 * Used to route join_request_received events.
 */
function getSpaceManagers(spaceId: string): string[] {
  const db = getDb();
  const space = db.select().from(schema.spaces).where(eq(schema.spaces.id, spaceId)).get();
  if (!space) return [];

  const memberRows = db.select()
    .from(schema.spaceMembers)
    .where(eq(schema.spaceMembers.spaceId, spaceId))
    .all();

  const managers: string[] = [];
  for (const member of memberRows) {
    if (member.userId === space.ownerId || hasPermission(member.userId, spaceId, PermissionBits.MANAGE_SPACE)) {
      managers.push(member.userId);
    }
  }
  return managers;
}

/**
 * Build a full SpaceWithChannelsAndMembers for a newly joined user.
 * Used after accepting a join request or public join.
 */
function buildFullSpace(spaceId: string, forUserId: string): SpaceWithChannelsAndMembers | null {
  const db = getDb();
  const space = db.select().from(schema.spaces).where(eq(schema.spaces.id, spaceId)).get();
  if (!space) return null;

  const channels = db.select()
    .from(schema.channels)
    .where(eq(schema.channels.spaceId, spaceId))
    .all();

  const categories = db.select()
    .from(schema.channelCategories)
    .where(eq(schema.channelCategories.spaceId, spaceId))
    .orderBy(schema.channelCategories.position)
    .all()
    .map(c => ({
      id: c.id,
      spaceId: c.spaceId,
      name: c.name,
      position: c.position ?? 0,
      createdAt: c.createdAt,
    }));

  const roles = db.select()
    .from(schema.roles)
    .where(eq(schema.roles.spaceId, spaceId))
    .orderBy(schema.roles.position)
    .all();

  const memberRows = db.select()
    .from(schema.spaceMembers)
    .where(eq(schema.spaceMembers.spaceId, spaceId))
    .all();

  const memberUserIds = memberRows.map(m => m.userId);
  const users = memberUserIds.length > 0
    ? db.select().from(schema.users).where(inArray(schema.users.id, memberUserIds)).all()
    : [];
  const userMap = new Map(users.map(u => [u.id, u]));

  const memberRoleRows = db.select()
    .from(schema.memberRoles)
    .where(eq(schema.memberRoles.spaceId, spaceId))
    .all();

  const members: MemberWithUser[] = memberRows
    .map(m => {
      const u = userMap.get(m.userId);
      if (!u) return null;

      const assignedRoleIds = memberRoleRows
        .filter(mr => mr.userId === m.userId)
        .map(mr => mr.roleId);

      const memberRoles = roles
        .filter(r => assignedRoleIds.includes(r.id))
        .map(r => ({
          id: r.id,
          spaceId: r.spaceId,
          name: r.name,
          color: r.color ?? '#b9bbbe',
          position: r.position ?? 0,
          createdAt: r.createdAt,
        }));

      return {
        spaceId: m.spaceId,
        userId: m.userId,
        nickname: m.nickname,
        joinedAt: m.joinedAt,
        user: sanitizeUser(u),
        roles: memberRoles,
      };
    })
    .filter((m): m is MemberWithUser => m !== null);

  const spacePerms = computePermissions(forUserId, spaceId);

  const visibleChannels: Channel[] = [];
  for (const ch of channels) {
    const chPerms = computePermissions(forUserId, spaceId, ch.id);
    const hasView = (chPerms & PermissionBits.VIEW_CHANNEL) !== 0n || (chPerms & PermissionBits.ADMINISTRATOR) !== 0n;
    if (hasView) {
      visibleChannels.push({
        id: ch.id,
        spaceId: ch.spaceId,
        name: ch.name,
        type: ch.type as Channel['type'],
        topic: ch.topic,
        position: ch.position ?? 0,
        categoryId: ch.categoryId ?? null,
        createdAt: ch.createdAt,
        myPermissions: permissionsToString(chPerms),
      });
    }
  }

  return {
    id: space.id,
    name: space.name,
    icon: space.icon,
    banner: space.banner ?? null,
    avatarColor: (space.avatarColor as SpaceWithChannelsAndMembers['avatarColor']) ?? null,
    ownerId: space.ownerId,
    inviteCode: space.inviteCode,
    visibility: (space.visibility ?? 'private') as SpaceWithChannelsAndMembers['visibility'],
    description: space.description ?? null,
    createdAt: space.createdAt,
    channels: visibleChannels,
    categories,
    members,
    roles: roles.map(r => ({
      id: r.id,
      spaceId: r.spaceId,
      name: r.name,
      color: r.color ?? '#b9bbbe',
      position: r.position ?? 0,
      permissions: r.permissions ?? undefined,
      isEveryone: r.id === spaceId,
      createdAt: r.createdAt,
    })),
    myPermissions: permissionsToString(spacePerms),
  };
}

export async function exploreRoutes(app: FastifyInstance): Promise<void> {

  // GET /api/spaces/explore — list discoverable spaces
  app.get<{ Querystring: { q?: string; limit?: string; offset?: string } }>('/api/spaces/explore', {
    preHandler: authenticate,
  }, async (request, reply) => {
    const db = getDb();

    // Check instance-level discovery toggle
    const settings = db.select().from(schema.instanceSettings).where(eq(schema.instanceSettings.id, 1)).get();
    if (!settings || settings.discoveryEnabled === 0) {
      return reply.code(200).send({ spaces: [], total: 0, discoveryEnabled: false });
    }

    const q = request.query.q?.trim() ?? '';
    const limit = Math.min(Math.max(parseInt(request.query.limit ?? '50', 10) || 50, 1), 100);
    const offset = Math.max(parseInt(request.query.offset ?? '0', 10) || 0, 0);

    // Get user's current space memberships to exclude
    const myMemberships = db.select({ spaceId: schema.spaceMembers.spaceId })
      .from(schema.spaceMembers)
      .where(eq(schema.spaceMembers.userId, request.userId))
      .all();
    const mySpaceIds = new Set(myMemberships.map(m => m.spaceId));

    // Build raw SQL query for explore — we need a LEFT JOIN for member count
    // which is more efficient to do with raw SQL
    const rawDb = getRawDb();

    // Count ALL discoverable spaces (including ones the user has joined) for context
    const totalAllRow = rawDb.prepare(
      `SELECT COUNT(DISTINCT s.id) as total FROM spaces s WHERE s.visibility IN ('public', 'request')`
    ).get() as { total: number };

    let countSql = `SELECT COUNT(DISTINCT s.id) as total FROM spaces s WHERE s.visibility IN ('public', 'request')`;
    let querySql = `
      SELECT s.id, s.name, s.icon, s.banner, s.avatar_color, s.description, s.visibility, s.created_at,
             COUNT(sm.user_id) as member_count
      FROM spaces s
      LEFT JOIN space_members sm ON sm.space_id = s.id
      WHERE s.visibility IN ('public', 'request')
    `;

    const params: (string | number)[] = [];

    if (q) {
      const likePattern = `%${q}%`;
      querySql += ` AND (s.name LIKE ? COLLATE NOCASE OR s.description LIKE ? COLLATE NOCASE)`;
      countSql += ` AND (s.name LIKE ? COLLATE NOCASE OR s.description LIKE ? COLLATE NOCASE)`;
      params.push(likePattern, likePattern);
    }

    querySql += ` GROUP BY s.id ORDER BY member_count DESC, s.created_at DESC LIMIT ? OFFSET ?`;

    const totalRow = rawDb.prepare(countSql).get(...params) as { total: number };
    const rows = rawDb.prepare(querySql).all(...params, limit, offset) as {
      id: string;
      name: string;
      icon: string | null;
      banner: string | null;
      avatar_color: string | null;
      description: string | null;
      visibility: string;
      created_at: number;
      member_count: number;
    }[];

    const spaces: ExploreSpace[] = rows.map(r => ({
      id: r.id,
      name: r.name,
      icon: r.icon,
      banner: r.banner,
      avatarColor: (r.avatar_color as ExploreSpace['avatarColor']) ?? null,
      description: r.description,
      visibility: r.visibility as ExploreSpace['visibility'],
      memberCount: r.member_count,
      createdAt: r.created_at,
      joined: mySpaceIds.has(r.id),
    }));

    return reply.code(200).send({ spaces, total: totalRow.total, totalAll: totalAllRow.total, discoveryEnabled: true });
  });

  // POST /api/spaces/:id/public-join — join a public space without invite
  app.post<{ Params: { id: string } }>('/api/spaces/:id/public-join', {
    preHandler: authenticate,
  }, async (request, reply) => {
    const { id } = request.params;
    const db = getDb();

    const space = db.select().from(schema.spaces).where(eq(schema.spaces.id, id)).get();
    if (!space) {
      return reply.code(404).send({ error: 'Space not found', statusCode: 404 });
    }

    if (space.visibility !== 'public') {
      return reply.code(403).send({ error: 'This space does not allow public joins', statusCode: 403 });
    }

    if (isBanned(id, request.userId)) {
      return reply.code(403).send({ error: 'You are banned from this space', statusCode: 403 });
    }

    if (isMember(id, request.userId)) {
      return reply.code(409).send({ error: 'You are already a member of this space', statusCode: 409 });
    }

    const now = Date.now();
    db.insert(schema.spaceMembers).values({
      spaceId: id,
      userId: request.userId,
      joinedAt: now,
    }).run();

    // Register in connectionManager for WS broadcasts
    connectionManager.addUserSpace(request.userId, id);

    // Broadcast member_joined to existing space members
    const joiningUser = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
    if (joiningUser) {
      const memberPayload: MemberWithUser = {
        spaceId: id,
        userId: request.userId,
        nickname: null,
        joinedAt: now,
        user: sanitizeUser(joiningUser),
        roles: [],
      };
      connectionManager.sendToSpace(id, {
        type: 'member_joined',
        spaceId: id,
        member: memberPayload,
      });
    }

    // Return full space data
    const fullSpace = buildFullSpace(id, request.userId);
    if (!fullSpace) {
      return reply.code(500).send({ error: 'Failed to load space', statusCode: 500 });
    }

    return reply.code(200).send(fullSpace);
  });

  // POST /api/spaces/:id/request-join — request to join a request-only space
  app.post<{ Params: { id: string }; Body: { message?: string } }>('/api/spaces/:id/request-join', {
    preHandler: authenticate,
    config: {
      rateLimit: {
        max: 5,
        timeWindow: '1 minute',
      },
    },
  }, async (request, reply) => {
    const { id } = request.params;
    const db = getDb();

    const space = db.select().from(schema.spaces).where(eq(schema.spaces.id, id)).get();
    if (!space) {
      return reply.code(404).send({ error: 'Space not found', statusCode: 404 });
    }

    if (space.visibility !== 'request') {
      return reply.code(403).send({ error: 'This space does not accept join requests', statusCode: 403 });
    }

    if (isBanned(id, request.userId)) {
      return reply.code(403).send({ error: 'You are banned from this space', statusCode: 403 });
    }

    if (isMember(id, request.userId)) {
      return reply.code(409).send({ error: 'You are already a member of this space', statusCode: 409 });
    }

    // Check for existing pending request
    const existingRequest = db.select().from(schema.joinRequests)
      .where(and(
        eq(schema.joinRequests.spaceId, id),
        eq(schema.joinRequests.userId, request.userId),
        eq(schema.joinRequests.status, 'pending'),
      ))
      .get();

    if (existingRequest) {
      return reply.code(409).send({ error: 'You already have a pending request for this space', statusCode: 409 });
    }

    const requestId = generateSnowflake();
    const now = Date.now();
    const msgText = request.body?.message?.trim().slice(0, 500) ?? null;

    db.insert(schema.joinRequests).values({
      id: requestId,
      spaceId: id,
      userId: request.userId,
      message: msgText,
      status: 'pending',
      createdAt: now,
    }).run();

    const joiningUser = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
    const joinRequest = rowToJoinRequest(
      db.select().from(schema.joinRequests).where(eq(schema.joinRequests.id, requestId)).get()!,
      joiningUser ? sanitizeUser(joiningUser) : undefined,
    );

    // Send WS event to space managers (owner + MANAGE_SPACE users)
    const managers = getSpaceManagers(id);
    for (const managerId of managers) {
      connectionManager.sendToUser(managerId, {
        type: 'join_request_received',
        request: joinRequest,
      });
    }

    return reply.code(201).send(joinRequest);
  });

  // GET /api/spaces/:id/join-requests — list join requests for a space
  app.get<{ Params: { id: string }; Querystring: { status?: string } }>('/api/spaces/:id/join-requests', {
    preHandler: authenticate,
  }, async (request, reply) => {
    const { id } = request.params;
    const db = getDb();

    const space = db.select().from(schema.spaces).where(eq(schema.spaces.id, id)).get();
    if (!space) {
      return reply.code(404).send({ error: 'Space not found', statusCode: 404 });
    }

    if (!isSpaceOwner(id, request.userId) && !hasPermission(request.userId, id, PermissionBits.MANAGE_SPACE)) {
      return reply.code(403).send({ error: 'Missing MANAGE_SPACE permission', statusCode: 403 });
    }

    const statusFilter = request.query.status ?? 'pending';
    if (!['pending', 'accepted', 'declined'].includes(statusFilter)) {
      return reply.code(400).send({ error: 'Status must be one of: pending, accepted, declined', statusCode: 400 });
    }
    const rows = db.select().from(schema.joinRequests)
      .where(and(
        eq(schema.joinRequests.spaceId, id),
        eq(schema.joinRequests.status, statusFilter),
      ))
      .all();

    // Populate user data
    const userIds = rows.map(r => r.userId);
    const users = userIds.length > 0
      ? db.select().from(schema.users).where(inArray(schema.users.id, userIds)).all()
      : [];
    const userMap = new Map(users.map(u => [u.id, sanitizeUser(u)]));

    const requests: JoinRequest[] = rows.map(r => rowToJoinRequest(r, userMap.get(r.userId)));

    return reply.code(200).send({ requests });
  });

  // PATCH /api/spaces/:id/join-requests/:requestId — accept or decline
  app.patch<{ Params: { id: string; requestId: string }; Body: { action: 'accept' | 'decline' } }>('/api/spaces/:id/join-requests/:requestId', {
    preHandler: authenticate,
  }, async (request, reply) => {
    const { id, requestId } = request.params;
    const { action } = request.body;
    const db = getDb();

    if (!action || (action !== 'accept' && action !== 'decline')) {
      return reply.code(400).send({ error: 'Action must be "accept" or "decline"', statusCode: 400 });
    }

    const space = db.select().from(schema.spaces).where(eq(schema.spaces.id, id)).get();
    if (!space) {
      return reply.code(404).send({ error: 'Space not found', statusCode: 404 });
    }

    if (!isSpaceOwner(id, request.userId) && !hasPermission(request.userId, id, PermissionBits.MANAGE_SPACE)) {
      return reply.code(403).send({ error: 'Missing MANAGE_SPACE permission', statusCode: 403 });
    }

    const joinReq = db.select().from(schema.joinRequests).where(eq(schema.joinRequests.id, requestId)).get();
    if (!joinReq || joinReq.spaceId !== id) {
      return reply.code(404).send({ error: 'Join request not found', statusCode: 404 });
    }

    if (joinReq.status !== 'pending') {
      return reply.code(400).send({ error: 'This request has already been decided', statusCode: 400 });
    }

    const now = Date.now();

    if (action === 'accept') {
      // Insert member and update request atomically
      db.transaction((tx) => {
        tx.insert(schema.spaceMembers).values({
          spaceId: id,
          userId: joinReq.userId,
          joinedAt: now,
        }).run();

        tx.update(schema.joinRequests).set({
          status: 'accepted',
          decidedBy: request.userId,
          decidedAt: now,
        }).where(eq(schema.joinRequests.id, requestId)).run();
      });

      // Register in connectionManager
      connectionManager.addUserSpace(joinReq.userId, id);

      // Broadcast member_joined to space
      const joiningUser = db.select().from(schema.users).where(eq(schema.users.id, joinReq.userId)).get();
      if (joiningUser) {
        const memberPayload: MemberWithUser = {
          spaceId: id,
          userId: joinReq.userId,
          nickname: null,
          joinedAt: now,
          user: sanitizeUser(joiningUser),
          roles: [],
        };
        connectionManager.sendToSpace(id, {
          type: 'member_joined',
          spaceId: id,
          member: memberPayload,
        });
      }

      // Build full space for the accepted user
      const fullSpace = buildFullSpace(id, joinReq.userId);

      const updatedRow = db.select().from(schema.joinRequests).where(eq(schema.joinRequests.id, requestId)).get()!;
      const updatedRequest = rowToJoinRequest(updatedRow, joiningUser ? sanitizeUser(joiningUser) : undefined);

      // Send join_request_accepted to the requesting user
      if (fullSpace) {
        connectionManager.sendToUser(joinReq.userId, {
          type: 'join_request_accepted',
          request: updatedRequest,
          space: fullSpace,
        });
      }

      return reply.code(200).send(updatedRequest);
    } else {
      // Decline
      db.update(schema.joinRequests).set({
        status: 'declined',
        decidedBy: request.userId,
        decidedAt: now,
      }).where(eq(schema.joinRequests.id, requestId)).run();

      const updatedRow = db.select().from(schema.joinRequests).where(eq(schema.joinRequests.id, requestId)).get()!;
      const updatedRequest = rowToJoinRequest(updatedRow);

      // Send join_request_declined to the requesting user
      connectionManager.sendToUser(joinReq.userId, {
        type: 'join_request_declined',
        request: updatedRequest,
      });

      return reply.code(200).send(updatedRequest);
    }
  });

  // GET /api/users/@me/join-requests — list the current user's join requests
  app.get<{ Querystring: { status?: string } }>('/api/users/@me/join-requests', {
    preHandler: authenticate,
  }, async (request, reply) => {
    const db = getDb();
    const statusFilter = request.query.status;

    let rows;
    if (statusFilter) {
      rows = db.select().from(schema.joinRequests)
        .where(and(
          eq(schema.joinRequests.userId, request.userId),
          eq(schema.joinRequests.status, statusFilter),
        ))
        .all();
    } else {
      rows = db.select().from(schema.joinRequests)
        .where(eq(schema.joinRequests.userId, request.userId))
        .all();
    }

    const requests: JoinRequest[] = rows.map(r => rowToJoinRequest(r));

    return reply.code(200).send({ requests });
  });
}
