import { aliasedTable } from 'drizzle-orm/alias';
import {
  and,
  asc,
  count,
  desc,
  eq,
  gte,
  ilike,
  isNull,
  lte,
  notInArray,
  or,
  sql
} from 'drizzle-orm';
import { announcements, memberships, users } from '@/db/schema';
import { canReadAllContent } from '@/features/content-approval/lib/workflow';
import { hasAnyPermission, hasPermission } from '@/lib/auth/authorization';
import type { Permission } from '@/lib/auth/permissions';
import { db } from '@/lib/db';
import type {
  AnnouncementAdmin,
  AnnouncementFilters,
  AnnouncementPublic,
  AnnouncementStatus
} from '../api/types';

type AnnouncementRow = {
  id: number;
  organizationId: string;
  title: string;
  content: string;
  attachmentFileName: string | null;
  attachmentFileType: string | null;
  attachmentFileSize: number | null;
  attachmentStorageKey: string | null;
  attachmentFileUrl: string | null;
  startDate: Date;
  endDate: Date | null;
  isPin: boolean;
  status: AnnouncementStatus;
  submittedAt: Date | null;
  submittedBy: string | null;
  reviewedAt: Date | null;
  reviewedBy: string | null;
  approvedAt: Date | null;
  approvedBy: string | null;
  publishedAt: Date | null;
  publishedBy: string | null;
  reviewComment: string | null;
  createdBy: string;
  createdByName: string | null;
  updatedBy: string | null;
  updatedByName: string | null;
  createdAt: Date;
  updatedAt: Date;
};

export type SerializedAnnouncementInternal = ReturnType<typeof serializeAnnouncementInternal>;

const announcementWorkflowPermissions = [
  'announcement:read_all',
  'announcement:approve',
  'announcement:revision',
  'announcement:reject',
  'announcement:publish',
  'announcement:view_history'
] as const satisfies readonly Permission[];

export function serializeAnnouncementInternal(row: AnnouncementRow) {
  return {
    id: row.id,
    organization_id: row.organizationId,
    title: row.title,
    content: row.content,
    attachment_file_name: row.attachmentFileName,
    attachment_file_type: row.attachmentFileType,
    attachment_file_size: row.attachmentFileSize,
    attachment_storage_key: row.attachmentStorageKey,
    attachment_file_url: row.attachmentStorageKey
      ? `/api/announcements/${row.id}/attachment`
      : row.attachmentFileUrl,
    start_date: row.startDate.toISOString(),
    end_date: row.endDate ? row.endDate.toISOString() : null,
    is_pin: row.isPin,
    status: row.status,
    submitted_at: row.submittedAt ? row.submittedAt.toISOString() : null,
    submitted_by: row.submittedBy,
    reviewed_at: row.reviewedAt ? row.reviewedAt.toISOString() : null,
    reviewed_by: row.reviewedBy,
    approved_at: row.approvedAt ? row.approvedAt.toISOString() : null,
    approved_by: row.approvedBy,
    published_at: row.publishedAt ? row.publishedAt.toISOString() : null,
    published_by: row.publishedBy,
    review_comment: row.reviewComment,
    created_by: row.createdBy,
    created_by_name: row.createdByName,
    created_at: row.createdAt.toISOString(),
    updated_at: row.updatedAt.toISOString()
  };
}

export function serializeAnnouncementPublic(row: AnnouncementRow): AnnouncementPublic {
  return {
    id: row.id,
    organization_id: row.organizationId,
    title: row.title,
    content: row.content,
    attachment_file_name: row.attachmentFileName,
    attachment_file_type: row.attachmentFileType,
    attachment_file_size: row.attachmentFileSize,
    attachment_storage_key: row.attachmentStorageKey,
    attachment_file_url: row.attachmentStorageKey
      ? `/api/announcements/${row.id}/attachment`
      : row.attachmentFileUrl,
    start_date: row.startDate.toISOString(),
    end_date: row.endDate ? row.endDate.toISOString() : null,
    is_pin: row.isPin,
    status: row.status,
    published_at: row.publishedAt ? row.publishedAt.toISOString() : null,
    created_by: row.createdBy,
    created_by_name: row.createdByName,
    created_at: row.createdAt.toISOString(),
    updated_at: row.updatedAt.toISOString()
  };
}

export function serializeAnnouncementAdmin(row: AnnouncementRow): AnnouncementAdmin {
  return {
    ...serializeAnnouncementPublic(row),
    submitted_at: row.submittedAt ? row.submittedAt.toISOString() : null,
    submitted_by: row.submittedBy,
    reviewed_at: row.reviewedAt ? row.reviewedAt.toISOString() : null,
    reviewed_by: row.reviewedBy,
    approved_at: row.approvedAt ? row.approvedAt.toISOString() : null,
    approved_by: row.approvedBy,
    published_by: row.publishedBy,
    review_comment: row.reviewComment
  };
}

function canReadAnnouncementWorkflowData({
  row,
  permissions,
  currentUserId
}: {
  row: AnnouncementRow;
  permissions: readonly string[] | null | undefined;
  currentUserId?: string | null;
}) {
  if (hasAnyPermission(permissions, announcementWorkflowPermissions)) {
    return true;
  }

  return (
    row.createdBy === currentUserId &&
    (row.status === 'returned' || row.status === 'rejected') &&
    hasPermission(permissions, 'announcement:edit_draft')
  );
}

export function serializeAnnouncementForAccess({
  row,
  permissions,
  currentUserId
}: {
  row: AnnouncementRow;
  permissions: readonly string[] | null | undefined;
  currentUserId?: string | null;
}) {
  if (canReadAnnouncementWorkflowData({ row, permissions, currentUserId })) {
    return serializeAnnouncementAdmin(row);
  }

  return serializeAnnouncementPublic(row);
}

export function isAnnouncementEligibleForPinning({
  status,
  startDate,
  endDate
}: {
  status: AnnouncementStatus;
  startDate: Date;
  endDate: Date | null;
}) {
  const now = new Date();

  if (status !== 'published') {
    return false;
  }

  if (startDate > now) {
    return false;
  }

  if (endDate && endDate < now) {
    return false;
  }

  return true;
}

function normalizeText(value?: string | null) {
  const normalized = value?.trim();
  return normalized ? normalized : null;
}

function getBaseSelect() {
  const creatorUsers = aliasedTable(users, 'creator_users');
  const updaterUsers = aliasedTable(users, 'updater_users');

  return {
    creatorUsers,
    updaterUsers,
    select: {
      id: announcements.id,
      organizationId: announcements.organizationId,
      title: announcements.title,
      content: announcements.content,
      attachmentFileName: announcements.attachmentFileName,
      attachmentFileType: announcements.attachmentFileType,
      attachmentFileSize: announcements.attachmentFileSize,
      attachmentStorageKey: announcements.attachmentStorageKey,
      attachmentFileUrl: announcements.attachmentFileUrl,
      startDate: announcements.startDate,
      endDate: announcements.endDate,
      isPin: announcements.isPin,
      status: announcements.status,
      submittedAt: announcements.submittedAt,
      submittedBy: announcements.submittedBy,
      reviewedAt: announcements.reviewedAt,
      reviewedBy: announcements.reviewedBy,
      approvedAt: announcements.approvedAt,
      approvedBy: announcements.approvedBy,
      publishedAt: announcements.publishedAt,
      publishedBy: announcements.publishedBy,
      reviewComment: announcements.reviewComment,
      createdBy: announcements.createdBy,
      createdByName: creatorUsers.name,
      updatedBy: announcements.updatedBy,
      updatedByName: updaterUsers.name,
      createdAt: announcements.createdAt,
      updatedAt: announcements.updatedAt
    }
  };
}

function getVisibleEmployeeClauses() {
  const now = new Date();

  return [
    eq(announcements.status, 'published'),
    lte(announcements.startDate, now),
    or(isNull(announcements.endDate), gte(announcements.endDate, now))!
  ];
}

function getPinnedVisibilityClauses() {
  const now = new Date();

  return [
    eq(announcements.isPin, true),
    eq(announcements.status, 'published' as AnnouncementStatus),
    lte(announcements.startDate, now),
    or(isNull(announcements.endDate), gte(announcements.endDate, now))!
  ];
}

function getActivePinnedOrderClause() {
  return sql<number>`case
    when ${announcements.isPin} = true
      and ${announcements.status} = 'published'
      and ${announcements.startDate} <= now()
      and (${announcements.endDate} is null or ${announcements.endDate} >= now())
    then 1
    else 0
  end`;
}

export async function listAnnouncementsForAccess({
  organizationId,
  permissions,
  currentUserId,
  filters
}: {
  organizationId: string;
  permissions: readonly string[] | null | undefined;
  currentUserId?: string | null;
  filters: AnnouncementFilters;
}) {
  const page = Number(filters.page ?? 1);
  const limit = Number(filters.limit ?? 10);
  const search = normalizeText(filters.search);
  const canManageAnnouncements = canReadAllContent('announcement', permissions);
  const { creatorUsers, updaterUsers, select } = getBaseSelect();

  const activePinnedOrder = getActivePinnedOrderClause();

  const clauses = [
    eq(announcements.organizationId, organizationId),
    ...(canManageAnnouncements ? [] : getVisibleEmployeeClauses()),
    ...(canManageAnnouncements && filters.status ? [eq(announcements.status, filters.status)] : []),
    ...(filters.pin === 'pinned' ? getPinnedVisibilityClauses() : []),
    ...(search
      ? [or(ilike(announcements.title, `%${search}%`), ilike(announcements.content, `%${search}%`))!]
      : [])
  ];

  const where = and(...clauses);
  const sortMap = {
    search: announcements.title,
    status: announcements.status,
    published_at: announcements.publishedAt,
    end_date: announcements.endDate,
    created_by_name: creatorUsers.name
  } as const;

  let orderByClauses = [desc(activePinnedOrder), desc(announcements.createdAt)] as Array<
    ReturnType<typeof asc> | ReturnType<typeof desc>
  >;

  if (filters.sort) {
    try {
      const sortItems = JSON.parse(filters.sort) as Array<{
        id: keyof typeof sortMap;
        desc: boolean;
      }>;

      const selectedSortClauses = sortItems
        .filter((item) => item.id in sortMap)
        .map((item) => (item.desc ? desc(sortMap[item.id]) : asc(sortMap[item.id])));

      if (selectedSortClauses.length > 0) {
        orderByClauses = [
          desc(activePinnedOrder),
          ...selectedSortClauses,
          desc(announcements.createdAt)
        ];
      }
    } catch {
      orderByClauses = [desc(activePinnedOrder), desc(announcements.createdAt)];
    }
  }

  const [totalResult] = await db.select({ value: count() }).from(announcements).where(where);
  const offset = (page - 1) * limit;

  const rows = await db
    .select(select)
    .from(announcements)
    .leftJoin(creatorUsers, eq(announcements.createdBy, creatorUsers.id))
    .leftJoin(updaterUsers, eq(announcements.updatedBy, updaterUsers.id))
    .where(where)
    .orderBy(...orderByClauses)
    .limit(limit)
    .offset(offset);

  return {
    offset,
    limit,
    total: totalResult?.value ?? 0,
    announcements: rows.map((row) =>
      serializeAnnouncementForAccess({
        row: row as AnnouncementRow,
        permissions,
        currentUserId
      })
    )
  };
}

export async function getAnnouncementByIdForAccess({
  id,
  organizationId,
  permissions
}: {
  id: number;
  organizationId: string;
  permissions: readonly string[] | null | undefined;
}) {
  const canManageAnnouncements = canReadAllContent('announcement', permissions);
  const { creatorUsers, updaterUsers, select } = getBaseSelect();
  const clauses = [
    eq(announcements.id, id),
    eq(announcements.organizationId, organizationId),
    ...(canManageAnnouncements ? [] : getVisibleEmployeeClauses())
  ];

  const [row] = await db
    .select(select)
    .from(announcements)
    .leftJoin(creatorUsers, eq(announcements.createdBy, creatorUsers.id))
    .leftJoin(updaterUsers, eq(announcements.updatedBy, updaterUsers.id))
    .where(and(...clauses))
    .limit(1);

  return row ? (row as AnnouncementRow) : null;
}

export async function getOrganizationAnnouncementRecipientIds(organizationId: string) {
  const rows = await db
    .select({ userId: memberships.userId })
    .from(memberships)
    .where(eq(memberships.organizationId, organizationId));

  return rows.map((row) => row.userId);
}

const announcementReviewerPermissions = [
  'announcement:approve',
  'announcement:publish',
  'announcement:revision',
  'announcement:reject'
] as const;

export async function getAnnouncementReviewRecipientIds({
  organizationId,
  excludeUserIds = []
}: {
  organizationId: string;
  excludeUserIds?: string[];
}) {
  const permissionClauses = announcementReviewerPermissions.map(
    (permission) => sql<boolean>`${permission} = ANY(${memberships.permissions})`
  );

  const rows = await db
    .selectDistinct({ userId: memberships.userId })
    .from(memberships)
    .innerJoin(users, eq(users.id, memberships.userId))
    .where(
      and(
        eq(memberships.organizationId, organizationId),
        eq(users.isActive, true),
        or(...permissionClauses)!,
        ...(excludeUserIds.length ? [notInArray(memberships.userId, excludeUserIds)] : [])
      )
    );

  return rows.map((row) => row.userId);
}

export async function getDashboardAnnouncementsForAccess({
  organizationId,
  permissions,
  currentUserId,
  limit = 5
}: {
  organizationId: string;
  permissions: readonly string[] | null | undefined;
  currentUserId?: string | null;
  limit?: number;
}) {
  const canManageAnnouncements = canReadAllContent('announcement', permissions);
  const { creatorUsers, updaterUsers, select } = getBaseSelect();
  const activePinnedOrder = getActivePinnedOrderClause();
  const clauses = [
    eq(announcements.organizationId, organizationId),
    ...(canManageAnnouncements ? [] : getVisibleEmployeeClauses())
  ];

  const rows = await db
    .select(select)
    .from(announcements)
    .leftJoin(creatorUsers, eq(announcements.createdBy, creatorUsers.id))
    .leftJoin(updaterUsers, eq(announcements.updatedBy, updaterUsers.id))
    .where(and(...clauses))
    .orderBy(desc(activePinnedOrder), desc(announcements.createdAt))
    .limit(limit);

  return rows.map((row) =>
    serializeAnnouncementForAccess({
      row: row as AnnouncementRow,
      permissions,
      currentUserId
    })
  );
}
