import { Body, Controller, ForbiddenException, Get, Inject, Param, Post, Query, UseGuards } from "@nestjs/common";
import type Redis from "ioredis";
import {
  DialPstnRequest,
  InviteToCallRequest,
  ListCallsQuery,
  MarkCallsSeenRequest,
  StartCallRequest,
  StartConnectRequest,
} from "@slackwsh/contracts";
import * as messaging from "@slackwsh/messaging";
import { AuthGuard } from "../auth/auth.guard";
import { CurrentUser } from "../auth/current-user.decorator";
import type { RequestUser } from "../auth/auth.guard";
import { REDIS_PUBSUB } from "../common/redis-pubsub.provider";
import { LiveKitService } from "./livekit.service";

@UseGuards(AuthGuard)
@Controller("workspaces/:workspaceId/calls")
export class CallsController {
  constructor(
    @Inject(REDIS_PUBSUB) private readonly redis: Redis,
    private readonly livekit: LiveKitService,
  ) {}

  @Post()
  async start(@CurrentUser() user: RequestUser, @Param("workspaceId") workspaceId: string, @Body() body: unknown) {
    const input = StartCallRequest.parse(body);
    return messaging.startCall(this.redis, workspaceId, user.userId, input);
  }

  @Post("connect")
  async connect(@CurrentUser() user: RequestUser, @Param("workspaceId") workspaceId: string, @Body() body: unknown) {
    const { channelId } = StartConnectRequest.parse(body);
    return messaging.startOrJoinConnect(this.redis, workspaceId, user.userId, channelId);
  }

  /** Paged rather than windowed (a call is an instant, not a range), and always
   * scoped to calls the reader took part in — see listCalls. */
  @Get()
  async list(@CurrentUser() user: RequestUser, @Param("workspaceId") workspaceId: string, @Query() query: unknown) {
    const filters = ListCallsQuery.parse(query);
    return { calls: await messaging.listCalls(this.redis, workspaceId, user.userId, filters) };
  }

  /** The people-to-call ranking. Separate from the member roster because it
   * answers a different question — who you actually talk to. */
  @Get("contacts")
  async contacts(@CurrentUser() user: RequestUser, @Param("workspaceId") workspaceId: string) {
    return { contacts: await messaging.callContacts(workspaceId, user.userId) };
  }

  @Get("unseen")
  async unseen(@CurrentUser() user: RequestUser, @Param("workspaceId") workspaceId: string) {
    return { unseen: await messaging.unseenMissedCallCount(workspaceId, user.userId) };
  }

  /** Bodyless POST is valid — acknowledging everything unseen needs no
   * arguments, and the web client omits the body in that case. */
  @Post("seen")
  async seen(@CurrentUser() user: RequestUser, @Param("workspaceId") workspaceId: string, @Body() body: unknown) {
    const { callIds } = MarkCallsSeenRequest.parse(body ?? {});
    return messaging.markCallsSeen(workspaceId, user.userId, callIds);
  }

  @Get(":callId")
  async get(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
  ) {
    return messaging.getCall(workspaceId, user.userId, callId);
  }

  @Post(":callId/media-token")
  async mediaToken(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
  ) {
    const onCall = await messaging.isCallParticipant(workspaceId, callId, user.userId);
    if (!onCall) throw new ForbiddenException("not on this call");
    const call = await messaging.getCall(workspaceId, user.userId, callId);
    const minted = await this.livekit.mintParticipantToken(call, user.userId);
    if (minted.configured && minted.roomName) {
      await messaging.bindLivekitRoom(workspaceId, user.userId, callId, minted.roomName);
    }
    return minted;
  }

  @Post(":callId/recording/start")
  async startRecording(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
  ) {
    const call = await messaging.getCall(workspaceId, user.userId, callId);
    return this.livekit.startRecording(call, user.userId);
  }

  @Post(":callId/recording/stop")
  async stopRecording(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
  ) {
    const call = await messaging.getCall(workspaceId, user.userId, callId);
    return this.livekit.stopRecording(call, user.userId);
  }

  @Post(":callId/dial")
  async dial(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
    @Body() body: unknown,
  ) {
    const { e164 } = DialPstnRequest.parse(body);
    const call = await messaging.getCall(workspaceId, user.userId, callId);
    return this.livekit.dialPstn(call, user.userId, e164);
  }

  @Post(":callId/join")
  async join(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
  ) {
    return messaging.joinCall(this.redis, workspaceId, user.userId, callId);
  }

  /** No target user in the route: you can only answer for yourself (see
   * declineCall in libs/messaging/src/calls.ts). */
  @Post(":callId/decline")
  async decline(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
  ) {
    return messaging.declineCall(this.redis, workspaceId, user.userId, callId);
  }

  @Post(":callId/leave")
  async leave(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
  ) {
    return messaging.leaveCall(this.redis, workspaceId, user.userId, callId);
  }

  @Post(":callId/end")
  async end(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
  ) {
    return messaging.endCall(this.redis, workspaceId, user.userId, callId);
  }

  @Post(":callId/invite")
  async invite(
    @CurrentUser() user: RequestUser,
    @Param("workspaceId") workspaceId: string,
    @Param("callId") callId: string,
    @Body() body: unknown,
  ) {
    const { inviteeUserIds } = InviteToCallRequest.parse(body);
    return messaging.inviteToCall(this.redis, workspaceId, user.userId, callId, inviteeUserIds);
  }
}
