import { aliasedTable } from 'drizzle-orm/alias';
import {
  and,
  asc,
  count,
  desc,
  eq,
  gte,
  ilike,
  inArray,
  lt,
  isNull,
  or,
  sql
} from 'drizzle-orm';
import {
  certificates,
  courses,
  departments,
  employees,
  trainingRecords,
  userEmployeeMaps,
  users
} from '@/db/schema';
import { db } from '@/lib/db';
import { getEmployeeIdsForUser } from '@/features/employees/server/employee-identity-linking';
import {
  approvalStatusValues,
  legacyTrainingTypeValues,
  normalizeTrainingTypeForDb,
  normalizeTrainingTypeForUi,
  uiTrainingTypeValues,
  type LegacyTrainingType
} from '../constants/training-record-options';
import type { TrainingRecordFilters, TrainingRecordMutationPayload } from '../api/types';
import { getDurationMinutes, minutesToDecimalHours } from '../utils/time-utils';

type TrainingRecordRow = {
  id: number;
  organizationId: string;
  userId: string | null;
  employeeId: number | null;
  employeeCode: string | null;
  userName: string;
  companyName: string | null;
  departmentName: string | null;
  courseId: number | null;
  courseName: string;
  trainingDate: Date;
  trainingType: LegacyTrainingType;
  submittedMinutes: number | null;
  hours: number;
  approvedMinutes: number | null;
  approvedHours: number | null;
  category: 'K' | 'S' | 'A' | null;
  organizer: string | null;
  note: string | null;
  certificatePreviewUrl: string | null;
  certificatePreviewType: string | null;
  approvalStatus: 'draft' | 'pending' | 'approved' | 'rejected' | 'needs_revision';
  reviewerNote: string | null;
  rejectReason: string | null;
  reviewedBy: string | null;
  reviewedAt: Date | null;
  createdBy: string;
  createdByName: string | null;
  createdAt: Date;
  updatedAt: Date;
};

const sortMap = {
  employee_code: employees.employeeCode,
  created_at: trainingRecords.createdAt,
  training_date: trainingRecords.trainingDate,
  user_name: employees.fullName,
  course_name: trainingRecords.courseName,
  hours: trainingRecords.hours,
  approval_status: trainingRecords.approvalStatus,
  updated_at: trainingRecords.updatedAt
} as const;

const previewableCertificateTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] as const;

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

export function isMissingTrainingRecordMinutesStorage(error: unknown) {
  if (!(error instanceof Error)) {
    return false;
  }

  const message = error.message.toLowerCase();

  return (
    message.includes('training_records') &&
    (message.includes('submitted_minutes') || message.includes('approved_minutes')) &&
    (message.includes('does not exist') ||
      message.includes('column') ||
      message.includes('failed query') ||
      message.includes('relation'))
  );
}

function canManageAllTrainingRecords(role: string) {
  return role === 'owner' || role === 'admin';
}

function buildTrainingRecordAccessClause({
  role,
  userId,
  employeeIds
}: {
  role: string;
  userId: string;
  employeeIds: number[];
}) {
  if (canManageAllTrainingRecords(role)) {
    return null;
  }

  const scopedClauses = [
    eq(trainingRecords.userId, userId),
    ...(employeeIds.length > 0 ? [inArray(trainingRecords.employeeId, employeeIds)] : []),
    and(eq(trainingRecords.submittedByUserId, userId), isNull(trainingRecords.userId))
  ];

  return or(...scopedClauses);
}

export function canReviewTrainingRecords(role: string) {
  return canManageAllTrainingRecords(role);
}

export async function canEmployeeManageEditableRecord({
  organizationId,
  role,
  recordUserId,
  recordEmployeeId,
  actorUserId,
  approvalStatus
}: {
  organizationId: string;
  role: string;
  recordUserId: string | null;
  recordEmployeeId: number | null;
  actorUserId: string;
  approvalStatus: TrainingRecordRow['approvalStatus'];
}) {
  if (canManageAllTrainingRecords(role)) {
    return true;
  }

  if (!(approvalStatus === 'draft' || approvalStatus === 'needs_revision')) {
    return false;
  }

  if (recordUserId === actorUserId) {
    return true;
  }

  if (recordEmployeeId === null) {
    return false;
  }

  const actorEmployeeIds = await getEmployeeIdsForUser({
    organizationId,
    userId: actorUserId
  });

  return (
    actorEmployeeIds.includes(recordEmployeeId)
  );
}

function toStatusFilter(statuses?: string | null) {
  if (!statuses) return [];

  const values = statuses
    .split(/[.,]/)
    .map((value) => value.trim())
    .filter(
      (value): value is TrainingRecordRow['approvalStatus'] =>
        (approvalStatusValues as readonly string[]).includes(value)
    );

  return values.length ? [inArray(trainingRecords.approvalStatus, values)] : [];
}

function toTypeFilter(types?: string | null) {
  if (!types) return [];

  const values = types
    .split(/[.,]/)
    .map((value) => value.trim())
    .filter((value) =>
      (uiTrainingTypeValues as readonly string[]).includes(value) ||
      (legacyTrainingTypeValues as readonly string[]).includes(value)
    )
    .map((value) => normalizeTrainingTypeForDb(value)) as TrainingRecordRow['trainingType'][];

  return values.length ? [inArray(trainingRecords.trainingType, values)] : [];
}

function toYearFilter(year?: string | null) {
  if (!year) return [];

  const normalized = year.trim();
  if (!/^\d{4}$/.test(normalized)) {
    return [];
  }

  const yearNumber = Number(normalized);
  const start = new Date(Date.UTC(yearNumber, 0, 1));
  const end = new Date(Date.UTC(yearNumber + 1, 0, 1));

  return [gte(trainingRecords.trainingDate, start), lt(trainingRecords.trainingDate, end)];
}

function toTextFilter(column: typeof employees.companyName | typeof departments.name, value?: string | null) {
  const normalized = value?.trim();
  return normalized ? [ilike(column, `%${normalized}%`)] : [];
}

export function serializeTrainingRecord(record: TrainingRecordRow) {
  return {
    id: record.id,
    organization_id: record.organizationId,
    user_id: record.userId,
    employee_id: record.employeeId,
    employee_code: record.employeeCode,
    user_name: record.userName,
    company_name: record.companyName,
    department_name: record.departmentName,
    course_id: record.courseId,
    course_name: record.courseName,
    training_date: record.trainingDate.toISOString(),
    training_type: normalizeTrainingTypeForUi(record.trainingType),
    submitted_minutes: getDurationMinutes(record.submittedMinutes, record.hours),
    hours: record.hours,
    approved_minutes: getDurationMinutes(record.approvedMinutes, record.approvedHours),
    approved_hours: record.approvedHours,
    category: record.category,
    organizer: record.organizer,
    note: record.note,
    certificate_preview_url: record.certificatePreviewUrl,
    certificate_preview_type: record.certificatePreviewType,
    approval_status: record.approvalStatus,
    reviewer_note: record.reviewerNote,
    reject_reason: record.rejectReason,
    reviewed_by: record.reviewedBy,
    reviewed_at: record.reviewedAt ? record.reviewedAt.toISOString() : null,
    created_by: record.createdBy,
    created_by_name: record.createdByName,
    created_at: record.createdAt.toISOString(),
    updated_at: record.updatedAt.toISOString()
  };
}

function buildTrainingRecordSelect(includeMinuteColumns = true) {
  const creatorUsers = aliasedTable(users, 'creator_users');

  return {
    creatorUsers,
    select: {
      id: trainingRecords.id,
      organizationId: trainingRecords.organizationId,
      userId: trainingRecords.userId,
      employeeId: trainingRecords.employeeId,
      employeeCode: employees.employeeCode,
      userName: employees.fullName,
      companyName: employees.companyName,
      departmentName: departments.name,
      courseId: trainingRecords.courseId,
      courseName: trainingRecords.courseName,
      trainingDate: trainingRecords.trainingDate,
      trainingType: trainingRecords.trainingType,
      submittedMinutes: includeMinuteColumns
        ? trainingRecords.submittedMinutes
        : sql<number | null>`null`.as('submitted_minutes'),
      hours: trainingRecords.hours,
      approvedMinutes: includeMinuteColumns
        ? trainingRecords.approvedMinutes
        : sql<number | null>`null`.as('approved_minutes'),
      approvedHours: trainingRecords.approvedHours,
      category: trainingRecords.category,
      organizer: trainingRecords.organizer,
      note: trainingRecords.note,
      approvalStatus: trainingRecords.approvalStatus,
      reviewerNote: trainingRecords.reviewerNote,
      rejectReason: trainingRecords.rejectReason,
      reviewedBy: trainingRecords.reviewedBy,
      reviewedAt: trainingRecords.reviewedAt,
      createdBy: trainingRecords.createdBy,
      createdByName: creatorUsers.name,
      createdAt: trainingRecords.createdAt,
      updatedAt: trainingRecords.updatedAt
    }
  };
}

export async function listTrainingRecordsForOrganization({
  organizationId,
  userId,
  role,
  filters
}: {
  organizationId: string;
  userId: string;
  role: string;
  filters: TrainingRecordFilters;
}) {
  const page = Number(filters.page ?? 1);
  const limit = Number(filters.limit ?? 10);
  const search = filters.search?.trim();
  const sort = filters.sort;
  const employeeIds = canManageAllTrainingRecords(role)
    ? []
    : await getEmployeeIdsForUser({ organizationId, userId });
  const accessClause = buildTrainingRecordAccessClause({
    role,
    userId,
    employeeIds
  });

  const clauses = [
    eq(trainingRecords.organizationId, organizationId),
    ...(accessClause ? [accessClause] : []),
    ...toStatusFilter(filters.statuses),
    ...toTypeFilter(filters.types),
    ...toYearFilter(filters.year),
    ...toTextFilter(employees.companyName, filters.company),
    ...toTextFilter(departments.name, filters.department),
    ...(search
      ? [
          or(
            ilike(employees.fullName, `%${search}%`),
            ilike(employees.employeeCode, `%${search}%`),
            ilike(employees.companyName, `%${search}%`),
            ilike(departments.name, `%${search}%`),
            ilike(trainingRecords.courseName, `%${search}%`),
            ilike(trainingRecords.organizer, `%${search}%`),
            ilike(trainingRecords.note, `%${search}%`)
          )!
        ]
      : [])
  ];

  if (!canManageAllTrainingRecords(role) && !accessClause) {
    return { offset: 0, limit, total: 0, trainingRecords: [] };
  }

  const where = clauses.length === 1 ? clauses[0] : and(...clauses);
  let orderByClause = desc(trainingRecords.createdAt);

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

      if (primarySort?.id && sortMap[primarySort.id]) {
        orderByClause = primarySort.desc ? desc(sortMap[primarySort.id]) : asc(sortMap[primarySort.id]);
      }
    } catch {
      orderByClause = desc(trainingRecords.createdAt);
    }
  }

  const buildBaseQuery = (includeMinuteColumns = true) => {
    const { creatorUsers, select } = buildTrainingRecordSelect(includeMinuteColumns);

    return db
      .select(select)
      .from(trainingRecords)
      .innerJoin(employees, eq(trainingRecords.employeeId, employees.id))
      .leftJoin(departments, eq(employees.departmentId, departments.id))
      .leftJoin(courses, eq(trainingRecords.courseId, courses.id))
      .innerJoin(creatorUsers, eq(trainingRecords.createdBy, creatorUsers.id));
  };

  const [totalResult] = await db
    .select({ value: count() })
    .from(trainingRecords)
    .innerJoin(employees, eq(trainingRecords.employeeId, employees.id))
    .leftJoin(departments, eq(employees.departmentId, departments.id))
    .where(where);

  const offset = (page - 1) * limit;
  let rows: TrainingRecordRow[] = [];

  try {
    rows = (await buildBaseQuery(true)
      .where(where)
      .orderBy(orderByClause)
      .limit(limit)
      .offset(offset)) as TrainingRecordRow[];
  } catch (error) {
    if (!isMissingTrainingRecordMinutesStorage(error)) {
      throw error;
    }

    rows = (await buildBaseQuery(false)
      .where(where)
      .orderBy(orderByClause)
      .limit(limit)
      .offset(offset)) as TrainingRecordRow[];
  }
  const trainingRecordIds = rows.map((row) => row.id);
  const certificateRows = trainingRecordIds.length
    ? await db.query.certificates.findMany({
        where: (table, { and, eq, inArray }) =>
          and(eq(table.organizationId, organizationId), inArray(table.trainingRecordId, trainingRecordIds)),
        orderBy: (table, { desc }) => [desc(table.uploadedAt)]
      })
    : [];

  const certificatePreviewMap = new Map<number, { fileUrl: string | null; fileType: string }>();

  for (const certificate of certificateRows) {
    if (!certificatePreviewMap.has(certificate.trainingRecordId)) {
      certificatePreviewMap.set(certificate.trainingRecordId, {
        fileUrl: previewableCertificateTypes.includes(certificate.fileType as (typeof previewableCertificateTypes)[number])
          ? `/api/training-records/${certificate.trainingRecordId}/certificates/${certificate.id}/download`
          : null,
        fileType: certificate.fileType
      });
    }
  }

  return {
    offset,
    limit,
    total: totalResult?.value ?? 0,
    trainingRecords: rows.map((row) =>
      serializeTrainingRecord({
        ...(row as TrainingRecordRow),
        certificatePreviewUrl: certificatePreviewMap.get(row.id)?.fileUrl ?? null,
        certificatePreviewType: certificatePreviewMap.get(row.id)?.fileType ?? null
      })
    )
  };
}

export async function getTrainingRecordByIdForOrganization({
  id,
  organizationId,
  userId,
  role
}: {
  id: number;
  organizationId: string;
  userId: string;
  role: string;
}) {
  const employeeIds = canManageAllTrainingRecords(role)
    ? []
    : await getEmployeeIdsForUser({ organizationId, userId });
  const accessClause = buildTrainingRecordAccessClause({
    role,
    userId,
    employeeIds
  });

  if (!canManageAllTrainingRecords(role) && !accessClause) {
    return null;
  }

  const clauses = [
    eq(trainingRecords.id, id),
    eq(trainingRecords.organizationId, organizationId),
    ...(accessClause ? [accessClause] : [])
  ];

  const buildDetailQuery = (includeMinuteColumns = true) => {
    const { creatorUsers, select } = buildTrainingRecordSelect(includeMinuteColumns);

    return db
      .select(select)
      .from(trainingRecords)
      .innerJoin(employees, eq(trainingRecords.employeeId, employees.id))
      .leftJoin(departments, eq(employees.departmentId, departments.id))
      .leftJoin(courses, eq(trainingRecords.courseId, courses.id))
      .innerJoin(creatorUsers, eq(trainingRecords.createdBy, creatorUsers.id))
      .where(and(...clauses))
      .limit(1);
  };

  let record: TrainingRecordRow | undefined;

  try {
    [record] = (await buildDetailQuery(true)) as TrainingRecordRow[];
  } catch (error) {
    if (!isMissingTrainingRecordMinutesStorage(error)) {
      throw error;
    }

    [record] = (await buildDetailQuery(false)) as TrainingRecordRow[];
  }

  if (!record) {
    return null;
  }

  const previewCertificate = await db.query.certificates.findFirst({
    where: and(eq(certificates.trainingRecordId, id), eq(certificates.organizationId, organizationId)),
    orderBy: (table, { desc }) => [desc(table.uploadedAt)]
  });

  return {
    ...(record as TrainingRecordRow),
    certificatePreviewUrl:
      previewCertificate &&
      previewableCertificateTypes.includes(previewCertificate.fileType as (typeof previewableCertificateTypes)[number])
        ? `/api/training-records/${id}/certificates/${previewCertificate.id}/download`
        : null,
    certificatePreviewType: previewCertificate?.fileType ?? null
  };
}

export async function assertUserAndCourseAccess({
  organizationId,
  targetEmployeeId,
  actorUserId,
  actorRole,
  courseId,
  courseName
}: {
  organizationId: string;
  targetEmployeeId: number;
  actorUserId: string;
  actorRole: string;
  courseId?: number | null;
  courseName: string;
}) {
  const employee = await db.query.employees.findFirst({
    where: and(eq(employees.id, targetEmployeeId), eq(employees.organizationId, organizationId))
  });

  const mappedUser = await db
    .select({ userId: userEmployeeMaps.userId })
    .from(userEmployeeMaps)
    .where(
      and(
        eq(userEmployeeMaps.organizationId, organizationId),
        eq(userEmployeeMaps.employeeId, targetEmployeeId)
      )
    )
    .limit(1);

  const actorEmployeeIds = canManageAllTrainingRecords(actorRole)
    ? []
    : await getEmployeeIdsForUser({ organizationId, userId: actorUserId });

  const canActOnTarget = canManageAllTrainingRecords(actorRole) || actorEmployeeIds.includes(targetEmployeeId);
  const normalizedCourseName = courseName.trim();

  if (!employee || !canActOnTarget) {
    return { user: null, course: null };
  }

  if (!normalizedCourseName && (courseId === null || courseId === undefined)) {
    return { user: { ...employee, userId: mappedUser[0]?.userId ?? null }, course: null };
  }

  const course =
    courseId !== null && courseId !== undefined
      ? await db.query.courses.findFirst({
          where: and(eq(courses.id, courseId), eq(courses.organizationId, organizationId))
        })
      : await db.query.courses.findFirst({
          where: and(eq(courses.name, normalizedCourseName), eq(courses.organizationId, organizationId))
        });

  return {
    user: { ...employee, userId: mappedUser[0]?.userId ?? null },
    course
  };
}

export function normalizeTrainingRecordPayload(payload: TrainingRecordMutationPayload) {
  const submittedMinutes = getDurationMinutes(payload.submittedMinutes, payload.hours) ?? 0;

  return {
    ...payload,
    courseId: payload.courseId ?? null,
    courseName: payload.courseName.trim(),
    submittedMinutes,
    hours: minutesToDecimalHours(submittedMinutes),
    trainingType: normalizeTrainingTypeForDb(payload.trainingType),
    organizer: normalizeOptionalText(payload.organizer),
    note: normalizeOptionalText(payload.note)
  };
}
