import { addMonths, isAfter } from 'date-fns';
import { and, asc, count, desc, eq, ilike, isNotNull, or } from 'drizzle-orm';
import {
  courses,
  departments,
  employees,
  positions,
  trainingMatrices,
  trainingRecords,
} from '@/db/schema';
import { db } from '@/lib/db';
import type {
  EmployeeCompliance,
  TrainingMatrixFilters,
  TrainingMatrixMutationPayload
} from '../api/types';

type TrainingMatrixRow = {
  id: number;
  organizationId: string;
  departmentId: number | null;
  departmentName: string | null;
  positionId: number | null;
  positionName: string | null;
  courseId: number;
  courseName: string;
  courseCategory: string;
  courseCertificateValidityMonths: number | null;
  isRequired: boolean;
  renewalPeriodMonths: number | null;
  note: string | null;
  createdAt: Date;
  updatedAt: Date;
};

const trainingMatrixSelect = {
  id: trainingMatrices.id,
  organizationId: trainingMatrices.organizationId,
  departmentId: trainingMatrices.departmentId,
  departmentName: departments.name,
  positionId: trainingMatrices.positionId,
  positionName: positions.name,
  courseId: trainingMatrices.courseId,
  courseName: courses.name,
  courseCategory: courses.category,
  courseCertificateValidityMonths: courses.certificateValidityMonths,
  isRequired: trainingMatrices.isRequired,
  renewalPeriodMonths: trainingMatrices.renewalPeriodMonths,
  note: trainingMatrices.note,
  createdAt: trainingMatrices.createdAt,
  updatedAt: trainingMatrices.updatedAt
};

const sortMap = {
  created_at: trainingMatrices.createdAt,
  course_name: courses.name,
  department_name: departments.name,
  position_name: positions.name,
  updated_at: trainingMatrices.updatedAt
} as const;

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

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

export function serializeTrainingMatrix(row: TrainingMatrixRow) {
  return {
    id: row.id,
    organization_id: row.organizationId,
    department_id: row.departmentId,
    department_name: row.departmentName,
    position_id: row.positionId,
    position_name: row.positionName,
    course_id: row.courseId,
    course_name: row.courseName,
    course_category: row.courseCategory,
    course_certificate_validity_months: row.courseCertificateValidityMonths,
    is_required: row.isRequired,
    renewal_period_months: row.renewalPeriodMonths,
    note: row.note,
    created_at: row.createdAt.toISOString(),
    updated_at: row.updatedAt.toISOString()
  };
}

export async function listTrainingMatricesForOrganization({
  organizationId,
  filters
}: {
  organizationId: string;
  filters: TrainingMatrixFilters;
}) {
  const page = Number(filters.page ?? 1);
  const limit = Number(filters.limit ?? 10);
  const search = filters.search?.trim();
  const sort = filters.sort;

  const clauses = [
    eq(trainingMatrices.organizationId, organizationId),
    ...(search
      ? [
          or(
            ilike(courses.name, `%${search}%`),
            ilike(courses.category, `%${search}%`),
            ilike(departments.name, `%${search}%`),
            ilike(positions.name, `%${search}%`),
            ilike(trainingMatrices.note, `%${search}%`)
          )!
        ]
      : [])
  ];

  const where = clauses.length === 1 ? clauses[0] : and(...clauses);

  let orderByClause = desc(trainingMatrices.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(trainingMatrices.createdAt);
    }
  }

  const baseQuery = db
    .select(trainingMatrixSelect)
    .from(trainingMatrices)
    .leftJoin(departments, eq(trainingMatrices.departmentId, departments.id))
    .leftJoin(positions, eq(trainingMatrices.positionId, positions.id))
    .innerJoin(courses, eq(trainingMatrices.courseId, courses.id));

  const [totalResult] = await db
    .select({ value: count() })
    .from(trainingMatrices)
    .leftJoin(departments, eq(trainingMatrices.departmentId, departments.id))
    .leftJoin(positions, eq(trainingMatrices.positionId, positions.id))
    .innerJoin(courses, eq(trainingMatrices.courseId, courses.id))
    .where(where);

  const offset = (page - 1) * limit;
  const rows = await baseQuery.where(where).orderBy(orderByClause).limit(limit).offset(offset);

  return {
    offset,
    limit,
    total: totalResult?.value ?? 0,
    matrices: rows.map((row) => serializeTrainingMatrix(row))
  };
}

export async function getTrainingMatrixByIdForOrganization({
  id,
  organizationId
}: {
  id: number;
  organizationId: string;
}) {
  const [row] = await db
    .select(trainingMatrixSelect)
    .from(trainingMatrices)
    .leftJoin(departments, eq(trainingMatrices.departmentId, departments.id))
    .leftJoin(positions, eq(trainingMatrices.positionId, positions.id))
    .innerJoin(courses, eq(trainingMatrices.courseId, courses.id))
    .where(and(eq(trainingMatrices.id, id), eq(trainingMatrices.organizationId, organizationId)))
    .limit(1);

  return row ?? null;
}

export async function getTrainingMatrixOptions(organizationId: string) {
  const [departmentRows, positionRows, courseRows] = await Promise.all([
    db
      .select()
      .from(departments)
      .where(and(eq(departments.organizationId, organizationId), eq(departments.isActive, true)))
      .orderBy(departments.name),
    db
      .select()
      .from(positions)
      .where(and(eq(positions.organizationId, organizationId), eq(positions.isActive, true)))
      .orderBy(positions.name),
    db
      .select()
      .from(courses)
      .where(and(eq(courses.organizationId, organizationId), eq(courses.isActive, true)))
      .orderBy(courses.name)
  ]);

  return {
    departments: departmentRows.map((department) => ({
      value: String(department.id),
      label: department.name
    })),
    positions: positionRows.map((position) => ({
      value: String(position.id),
      label: position.name
    })),
    courses: courseRows.map((course) => ({
      value: String(course.id),
      label: `${course.name} (${course.category})`
    }))
  };
}

function mergeApplicableRules(rows: TrainingMatrixRow[]) {
  const map = new Map<
    number,
    {
      courseName: string;
      expiryMonths: number | null;
    }
  >();

  for (const row of rows) {
    if (!row.isRequired) continue;

    const expiryCandidate =
      row.renewalPeriodMonths ?? row.courseCertificateValidityMonths ?? null;
    const current = map.get(row.courseId);

    if (!current) {
      map.set(row.courseId, {
        courseName: row.courseName,
        expiryMonths: expiryCandidate
      });
      continue;
    }

    if (current.expiryMonths === null) {
      map.set(row.courseId, {
        courseName: row.courseName,
        expiryMonths: expiryCandidate
      });
      continue;
    }

    if (expiryCandidate !== null && expiryCandidate < current.expiryMonths) {
      map.set(row.courseId, {
        courseName: row.courseName,
        expiryMonths: expiryCandidate
      });
    }
  }

  return map;
}

export async function getTrainingMatrixComplianceForOrganization(
  organizationId: string
): Promise<EmployeeCompliance[]> {
  const [employeeRows, matrixRows, approvedRecordRows] = await Promise.all([
    db
      .select({
        id: employees.id,
        employeeCode: employees.employeeCode,
        fullName: employees.fullName,
        departmentId: employees.departmentId,
        departmentName: departments.name,
        positionId: employees.positionId,
        positionName: positions.name
      })
      .from(employees)
      .leftJoin(departments, eq(employees.departmentId, departments.id))
      .leftJoin(positions, eq(employees.positionId, positions.id))
      .where(and(eq(employees.organizationId, organizationId), eq(employees.isActive, true))),
    db
      .select(trainingMatrixSelect)
      .from(trainingMatrices)
      .leftJoin(departments, eq(trainingMatrices.departmentId, departments.id))
      .leftJoin(positions, eq(trainingMatrices.positionId, positions.id))
      .innerJoin(courses, eq(trainingMatrices.courseId, courses.id))
      .where(eq(trainingMatrices.organizationId, organizationId)),
    db
      .select({
        employeeId: trainingRecords.employeeId,
        courseId: trainingRecords.courseId,
        trainingDate: trainingRecords.trainingDate
      })
      .from(trainingRecords)
      .where(
        and(
          eq(trainingRecords.organizationId, organizationId),
          eq(trainingRecords.approvalStatus, 'approved'),
          isNotNull(trainingRecords.courseId),
          isNotNull(trainingRecords.employeeId)
        )
      )
  ]);

  const latestApprovedMap = new Map<string, Date>();

  for (const record of approvedRecordRows) {
    const key = `${record.employeeId}:${record.courseId}`;
    const current = latestApprovedMap.get(key);

    if (!current || isAfter(record.trainingDate, current)) {
      latestApprovedMap.set(key, record.trainingDate);
    }
  }

  return employeeRows.map((employee) => {
    const applicableRows = matrixRows.filter((row) => {
      const matchesDepartment = row.departmentId !== null && row.departmentId === employee.departmentId;
      const matchesPosition = row.positionId !== null && row.positionId === employee.positionId;
      return matchesDepartment || matchesPosition;
    });

    const requiredCourses = mergeApplicableRules(applicableRows);
    let completedCourses = 0;
    const missingCourses: string[] = [];

    for (const [courseId, rule] of requiredCourses) {
      const approvedAt = latestApprovedMap.get(`${employee.id}:${courseId}`);

      if (!approvedAt) {
        missingCourses.push(rule.courseName);
        continue;
      }

      const isValid =
        rule.expiryMonths === null ? true : isAfter(addMonths(approvedAt, rule.expiryMonths), new Date());

      if (isValid) {
        completedCourses += 1;
      } else {
        missingCourses.push(rule.courseName);
      }
    }

    const requiredCount = requiredCourses.size;
    const compliancePercent =
      requiredCount === 0 ? 100 : Math.round((completedCourses / requiredCount) * 100);

    return {
      employee_id: employee.id,
      employee_name: employee.fullName,
      employee_code: employee.employeeCode,
      department_name: employee.departmentName,
      position_name: employee.positionName,
      required_courses: requiredCount,
      completed_courses: completedCourses,
      compliance_percent: compliancePercent,
      missing_courses: missingCourses
    };
  });
}

export function normalizeTrainingMatrixPayload(payload: TrainingMatrixMutationPayload) {
  return {
    ...payload,
    departmentId: payload.departmentId ?? null,
    positionId: payload.positionId ?? null,
    renewalPeriodMonths:
      payload.renewalPeriodMonths === undefined ? null : payload.renewalPeriodMonths,
    note: normalizeOptionalText(payload.note)
  };
}
