import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from "@nestjs/common";
import { and, asc, eq, gt } from "drizzle-orm";
import Redis from "ioredis";
import { schema, withTenant } from "@slackwsh/data";
import { activeWorkspaceMemberIds, fanoutToMembers } from "@slackwsh/messaging";
import type {
  CreateScheduledStatusRequest,
  SetStatusRequest,
  UpdateAutomaticStatusPreferencesRequest,
} from "@slackwsh/contracts";
import { REDIS_PUBSUB } from "../common/redis-pubsub.provider";

type Member = typeof schema.workspaceMembers.$inferSelect;
type Schedule = typeof schema.scheduledStatuses.$inferSelect;

export interface EffectiveStatus {
  text: string | null;
  emoji: string | null;
  expiresAt: string | null;
  source: "manual" | "scheduled" | "connect" | "focus" | "outside_hours" | null;
  dndActive: boolean;
  dndUntil: string | null;
}

function localClock(now: Date, timezone: string) {
  try {
    const parts = new Intl.DateTimeFormat("en-US", {
      timeZone: timezone,
      weekday: "short",
      hour: "2-digit",
      minute: "2-digit",
      hour12: false,
    }).formatToParts(now);
    const value = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value ?? "";
    const day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(value("weekday"));
    return { day, minutes: Number(value("hour")) * 60 + Number(value("minute")) };
  } catch {
    return { day: now.getDay(), minutes: now.getHours() * 60 + now.getMinutes() };
  }
}

function timeMinutes(value: string) {
  const [hours, minutes] = value.split(":").map(Number);
  return (hours ?? 0) * 60 + (minutes ?? 0);
}

function outsideWorkingHours(member: Member, timezone: string, now: Date) {
  const { day, minutes } = localClock(now, timezone);
  const workingDays = Array.isArray(member.workingDays) ? member.workingDays.map(Number) : [1, 2, 3, 4, 5];
  if (!workingDays.includes(day)) return true;
  const start = timeMinutes(member.workHoursStart);
  const end = timeMinutes(member.workHoursEnd);
  if (start === end) return false;
  if (start < end) return minutes < start || minutes >= end;
  return minutes >= end && minutes < start;
}

export function resolveEffectiveStatus(member: Member, schedules: Schedule[], timezone: string, now = new Date()): EffectiveStatus {
  const focusDnd = member.focusModeEnabled;
  const manualActive = Boolean(member.statusText?.trim()) && (!member.statusExpiresAt || member.statusExpiresAt > now);
  if (manualActive) {
    const statusDnd = member.dndEnabled && (!member.dndUntil || member.dndUntil > now);
    const dndActive = statusDnd || focusDnd;
    return {
      text: member.statusText,
      emoji: member.statusEmoji || "💬",
      expiresAt: member.statusExpiresAt?.toISOString() ?? null,
      source: "manual",
      dndActive,
      dndUntil: statusDnd ? member.dndUntil?.toISOString() ?? null : null,
    };
  }

  const scheduled = schedules
    .filter((row) => row.startsAt <= now && row.endsAt > now)
    .sort((a, b) => b.startsAt.getTime() - a.startsAt.getTime())[0];
  if (scheduled) {
    return {
      text: scheduled.text,
      emoji: scheduled.emoji || "💬",
      expiresAt: scheduled.endsAt.toISOString(),
      source: "scheduled",
      dndActive: scheduled.pauseNotifications || focusDnd,
      dndUntil: scheduled.pauseNotifications ? scheduled.endsAt.toISOString() : null,
    };
  }

  if (member.autoStatusConnect && member.inConnect) {
    return { text: "In Connect", emoji: "🎧", expiresAt: null, source: "connect", dndActive: focusDnd, dndUntil: null };
  }
  if (member.focusModeEnabled) {
    return member.autoStatusFocus
      ? { text: "In focus mode", emoji: "🎯", expiresAt: null, source: "focus", dndActive: true, dndUntil: null }
      : { text: null, emoji: null, expiresAt: null, source: null, dndActive: true, dndUntil: null };
  }
  if (member.autoStatusOutsideHours && member.availabilityMode !== "available" && outsideWorkingHours(member, timezone, now)) {
    return { text: "Outside working hours", emoji: "🌙", expiresAt: null, source: "outside_hours", dndActive: false, dndUntil: null };
  }
  return { text: null, emoji: null, expiresAt: null, source: null, dndActive: false, dndUntil: null };
}

@Injectable()
export class StatusService {
  constructor(@Inject(REDIS_PUBSUB) private readonly redis: Redis) {}

  async resolveMemberRows(workspaceId: number, actorUserId: number, rows: Array<{ member: Member; user: typeof schema.users.$inferSelect }>) {
    const schedules = await withTenant({ workspaceId, userId: actorUserId }, (tx) =>
      tx
        .select()
        .from(schema.scheduledStatuses)
        .where(and(eq(schema.scheduledStatuses.workspaceId, workspaceId), gt(schema.scheduledStatuses.endsAt, new Date())))
        .orderBy(asc(schema.scheduledStatuses.startsAt)),
    );
    const byUser = new Map<number, Schedule[]>();
    for (const schedule of schedules) byUser.set(schedule.userId, [...(byUser.get(schedule.userId) ?? []), schedule]);
    return rows.map((row) => {
      const effective = resolveEffectiveStatus(row.member, byUser.get(row.user.id) ?? [], row.user.tz);
      return {
        ...row,
        member: {
          ...row.member,
          statusText: effective.text,
          statusEmoji: effective.emoji,
          statusExpiresAt: effective.expiresAt,
          statusSource: effective.source,
          dndActive: effective.dndActive,
          dndUntil: effective.dndUntil,
        },
      };
    });
  }

  async getMine(workspaceId: number, userId: number) {
    const { member, timezone, schedules } = await withTenant({ workspaceId, userId }, async (tx) => {
      const [row] = await tx
        .select({ member: schema.workspaceMembers, timezone: schema.users.tz })
        .from(schema.workspaceMembers)
        .innerJoin(schema.users, eq(schema.users.id, schema.workspaceMembers.userId))
        .where(and(eq(schema.workspaceMembers.workspaceId, workspaceId), eq(schema.workspaceMembers.userId, userId)))
        .limit(1);
      if (!row?.member || row.member.deactivatedAt) throw new ForbiddenException("not an active workspace member");
      const scheduled = await tx
        .select()
        .from(schema.scheduledStatuses)
        .where(
          and(
            eq(schema.scheduledStatuses.workspaceId, workspaceId),
            eq(schema.scheduledStatuses.userId, userId),
            gt(schema.scheduledStatuses.endsAt, new Date()),
          ),
        )
        .orderBy(asc(schema.scheduledStatuses.startsAt));
      return { member: row.member, timezone: row.timezone, schedules: scheduled };
    });
    const effective = resolveEffectiveStatus(member, schedules, timezone);
    return {
      manual: {
        text: member.statusText,
        emoji: member.statusEmoji,
        expiresAt: member.statusExpiresAt?.toISOString() ?? null,
        pauseNotifications: member.dndEnabled,
      },
      effective,
      availabilityMode: member.availabilityMode === "away" ? "away" : member.availabilityMode === "available" ? "available" : "auto",
      automatic: {
        connect: member.autoStatusConnect,
        focus: member.autoStatusFocus,
        outsideWorkingHours: member.autoStatusOutsideHours,
        focusModeEnabled: member.focusModeEnabled,
        inConnect: member.inConnect,
        workHoursStart: member.workHoursStart,
        workHoursEnd: member.workHoursEnd,
        workingDays: Array.isArray(member.workingDays) ? member.workingDays : [1, 2, 3, 4, 5],
      },
      schedules: schedules.map((row) => ({
        id: row.id,
        text: row.text,
        emoji: row.emoji,
        startsAt: row.startsAt.toISOString(),
        endsAt: row.endsAt.toISOString(),
        pauseNotifications: row.pauseNotifications,
      })),
    };
  }

  async setStatus(workspaceId: number, userId: number, input: SetStatusRequest) {
    const text = input.text.trim();
    const expiresAt = input.expiresAt ? new Date(input.expiresAt) : null;
    if (expiresAt && expiresAt <= new Date()) throw new BadRequestException("status expiration must be in the future");
    await this.updateMember(workspaceId, userId, {
      statusText: text || null,
      statusEmoji: text ? input.emoji.trim() || "💬" : null,
      statusExpiresAt: text ? expiresAt : null,
      dndEnabled: text ? input.pauseNotifications : false,
      dndUntil: text && input.pauseNotifications ? expiresAt : null,
    });
    await this.changed(workspaceId, userId);
    return this.getMine(workspaceId, userId);
  }

  async setAvailability(workspaceId: number, userId: number, mode: "auto" | "away" | "available") {
    const values: Partial<Member> = { availabilityMode: mode };
    if (mode === "available") {
      values.statusText = null;
      values.statusEmoji = null;
      values.statusExpiresAt = null;
      values.dndEnabled = false;
      values.dndUntil = null;
    }
    await this.updateMember(workspaceId, userId, values);
    await this.changed(workspaceId, userId);
    return this.getMine(workspaceId, userId);
  }

  async setContext(workspaceId: number, userId: number, patch: { focusModeEnabled?: boolean; inConnect?: boolean }) {
    const values: Partial<Member> = {};
    if (patch.focusModeEnabled !== undefined) values.focusModeEnabled = patch.focusModeEnabled;
    if (patch.inConnect !== undefined) values.inConnect = patch.inConnect;
    if (Object.keys(values).length > 0) await this.updateMember(workspaceId, userId, values);
    await this.changed(workspaceId, userId);
    return this.getMine(workspaceId, userId);
  }

  async updateAutomatic(workspaceId: number, userId: number, input: UpdateAutomaticStatusPreferencesRequest) {
    await this.updateMember(workspaceId, userId, {
      autoStatusConnect: input.connect,
      autoStatusFocus: input.focus,
      autoStatusOutsideHours: input.outsideWorkingHours,
      workHoursStart: input.workHoursStart,
      workHoursEnd: input.workHoursEnd,
      workingDays: [...new Set(input.workingDays)].sort(),
    });
    await this.changed(workspaceId, userId);
    return this.getMine(workspaceId, userId);
  }

  async createSchedule(workspaceId: number, userId: number, input: CreateScheduledStatusRequest) {
    const startsAt = new Date(input.startsAt);
    const endsAt = new Date(input.endsAt);
    if (endsAt <= startsAt || endsAt <= new Date()) throw new BadRequestException("scheduled status must end after it starts");
    await withTenant({ workspaceId, userId }, async (tx) => {
      await this.requireMember(tx, workspaceId, userId);
      const future = await tx
        .select({ id: schema.scheduledStatuses.id })
        .from(schema.scheduledStatuses)
        .where(
          and(
            eq(schema.scheduledStatuses.workspaceId, workspaceId),
            eq(schema.scheduledStatuses.userId, userId),
            gt(schema.scheduledStatuses.endsAt, new Date()),
          ),
        );
      if (future.length >= 5) throw new BadRequestException("you can schedule up to five statuses");
      await tx.insert(schema.scheduledStatuses).values({
        workspaceId,
        userId,
        text: input.text,
        emoji: input.emoji || "💬",
        startsAt,
        endsAt,
        pauseNotifications: input.pauseNotifications,
      });
    });
    await this.changed(workspaceId, userId);
    return this.getMine(workspaceId, userId);
  }

  async updateSchedule(workspaceId: number, userId: number, scheduleId: number, input: CreateScheduledStatusRequest) {
    const startsAt = new Date(input.startsAt);
    const endsAt = new Date(input.endsAt);
    if (endsAt <= startsAt || endsAt <= new Date()) throw new BadRequestException("scheduled status must end after it starts");
    await withTenant({ workspaceId, userId }, async (tx) => {
      const [updated] = await tx
        .update(schema.scheduledStatuses)
        .set({ text: input.text, emoji: input.emoji || "💬", startsAt, endsAt, pauseNotifications: input.pauseNotifications })
        .where(
          and(
            eq(schema.scheduledStatuses.id, scheduleId),
            eq(schema.scheduledStatuses.workspaceId, workspaceId),
            eq(schema.scheduledStatuses.userId, userId),
          ),
        )
        .returning();
      if (!updated) throw new NotFoundException("scheduled status not found");
    });
    await this.changed(workspaceId, userId);
    return this.getMine(workspaceId, userId);
  }

  async deleteSchedule(workspaceId: number, userId: number, scheduleId: number) {
    await withTenant({ workspaceId, userId }, async (tx) => {
      const [deleted] = await tx
        .delete(schema.scheduledStatuses)
        .where(
          and(
            eq(schema.scheduledStatuses.id, scheduleId),
            eq(schema.scheduledStatuses.workspaceId, workspaceId),
            eq(schema.scheduledStatuses.userId, userId),
          ),
        )
        .returning();
      if (!deleted) throw new NotFoundException("scheduled status not found");
    });
    await this.changed(workspaceId, userId);
    return this.getMine(workspaceId, userId);
  }

  private async updateMember(workspaceId: number, userId: number, values: Partial<Member>) {
    await withTenant({ workspaceId, userId }, async (tx) => {
      await this.requireMember(tx, workspaceId, userId);
      await tx
        .update(schema.workspaceMembers)
        .set(values)
        .where(and(eq(schema.workspaceMembers.workspaceId, workspaceId), eq(schema.workspaceMembers.userId, userId)));
    });
  }

  private async requireMember(tx: any, workspaceId: number, userId: number) {
    const [member] = await tx
      .select()
      .from(schema.workspaceMembers)
      .where(and(eq(schema.workspaceMembers.workspaceId, workspaceId), eq(schema.workspaceMembers.userId, userId)))
      .limit(1);
    if (!member || member.deactivatedAt) throw new ForbiddenException("not an active workspace member");
    return member as Member;
  }

  private async changed(workspaceId: number, userId: number) {
    const recipients = await withTenant({ workspaceId, userId }, (tx) =>
      activeWorkspaceMemberIds(tx, workspaceId),
    );

    // Every authenticated socket joins its personal room. Fan-out there so a
    // status change arrives even when the recipient has not opened a channel
    // (and therefore has not joined the workspace room yet).
    await fanoutToMembers(this.redis, recipients, {
      type: "member:status_changed",
      payload: { workspaceId, userId },
    });
  }
}
