import {
  and,
  asc,
  count,
  desc,
  eq,
  ilike,
  inArray,
  ne,
  or,
  sql
} from 'drizzle-orm';
import { auditLogs, trainingPolicies, users } from '@/db/schema';
import { formatAuditDetails } from '@/features/audit-logs/server/audit-log-data';
import { AUDIT_MODULE } from '@/features/audit-logs/server/audit-service';
import { db } from '@/lib/db';
import type {
  TrainingPolicyFilters,
  TrainingPolicyHistoryEntry,
  TrainingPolicyMutationPayload
} from '../api/types';
import {
  DEFAULT_TRAINING_POLICY,
  getDefaultTrainingPolicyYear
} from '../constants/training-policy-defaults';

type TrainingPolicyRow = typeof trainingPolicies.$inferSelect;

const sortMap = {
  year: trainingPolicies.year,
  total_hours: trainingPolicies.totalHours,
  updated_at: trainingPolicies.updatedAt,
  status: trainingPolicies.isActive
} as const;

function normalizeNumber(value: number | null | undefined) {
  if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value)) {
    return 0;
  }

  return Number(value.toFixed(2));
}

function normalizeIsoDate(value: Date | string | null | undefined) {
  if (value instanceof Date) {
    return value.toISOString();
  }

  if (typeof value === 'string') {
    const parsed = new Date(value);

    if (!Number.isNaN(parsed.getTime())) {
      return parsed.toISOString();
    }
  }

  return new Date(0).toISOString();
}

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

function buildTrainingPolicyDisplayName(year: number) {
  return `Training Policy ${year}`;
}

async function getLatestAuditUsersByPolicyIds(policyIds: number[]) {
  if (policyIds.length === 0) {
    return new Map<number, string | null>();
  }

  const rows = await db
    .select({
      entityId: auditLogs.entityId,
      userName: users.name
    })
    .from(auditLogs)
    .leftJoin(users, eq(auditLogs.userId, users.id))
    .where(
      and(
        eq(auditLogs.moduleName, AUDIT_MODULE.TRAINING_POLICY),
        inArray(
          auditLogs.entityId,
          policyIds.map((policyId) => String(policyId))
        )
      )
    )
    .orderBy(desc(auditLogs.createdAt));

  const result = new Map<number, string | null>();

  for (const row of rows) {
    const policyId = Number(row.entityId);

    if (!Number.isFinite(policyId) || result.has(policyId)) {
      continue;
    }

    result.set(policyId, row.userName ?? null);
  }

  return result;
}

export function serializeTrainingPolicy(
  policy: TrainingPolicyRow,
  options?: {
    updatedByName?: string | null;
  }
) {
  return {
    id: policy.id,
    organization_id: policy.organizationId,
    year: policy.year,
    total_hours: normalizeNumber(policy.totalHours),
    k_hours: normalizeNumber(policy.kHours),
    s_hours: normalizeNumber(policy.sHours),
    a_hours: normalizeNumber(policy.aHours),
    is_active: policy.isActive,
    status: policy.isActive ? 'active' : 'inactive',
    display_name: buildTrainingPolicyDisplayName(policy.year),
    updated_by_name: options?.updatedByName ?? null,
    created_at: normalizeIsoDate(policy.createdAt),
    updated_at: normalizeIsoDate(policy.updatedAt)
  };
}

export async function listTrainingPoliciesForOrganization({
  organizationId,
  filters
}: {
  organizationId: string;
  filters: TrainingPolicyFilters;
}) {
  const page = Number(filters.page ?? 1);
  const limit = Number(filters.limit ?? 10);
  const search = normalizeText(filters.search);
  const status = normalizeText(filters.status);

  const clauses = [
    eq(trainingPolicies.organizationId, organizationId),
    ...(search
      ? [
          or(
            ilike(sql`CAST(${trainingPolicies.year} AS TEXT)`, `%${search}%`),
            ilike(sql`CAST(${trainingPolicies.totalHours} AS TEXT)`, `%${search}%`)
          )!
        ]
      : []),
    ...(status === 'active'
      ? [eq(trainingPolicies.isActive, true)]
      : status === 'inactive'
        ? [eq(trainingPolicies.isActive, false)]
        : [])
  ];

  const where = clauses.length > 1 ? and(...clauses) : clauses[0];
  let orderByClause = desc(trainingPolicies.updatedAt);

  if (filters.sort) {
    try {
      const sortItems = JSON.parse(filters.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(trainingPolicies.updatedAt);
    }
  }

  const baseQuery = db.select().from(trainingPolicies).where(where);
  const countSubquery = baseQuery.as('scoped_training_policies');
  const [totalResult] = await db.select({ value: count() }).from(countSubquery);
  const offset = (page - 1) * limit;
  const rows = await baseQuery
    .orderBy(orderByClause, desc(trainingPolicies.year))
    .limit(limit)
    .offset(offset);

  const latestAuditUsersByPolicy = await getLatestAuditUsersByPolicyIds(rows.map((row) => row.id));

  const [activePolicy] = await db
    .select()
    .from(trainingPolicies)
    .where(
      and(
        eq(trainingPolicies.organizationId, organizationId),
        eq(trainingPolicies.isActive, true)
      )
    )
    .orderBy(desc(trainingPolicies.updatedAt), desc(trainingPolicies.year))
    .limit(1);

  const activePolicyAuditUsers = activePolicy
    ? await getLatestAuditUsersByPolicyIds([activePolicy.id])
    : new Map<number, string | null>();

  return {
    offset,
    limit,
    total: totalResult?.value ?? 0,
    activePolicy: activePolicy
      ? serializeTrainingPolicy(activePolicy, {
          updatedByName: activePolicyAuditUsers.get(activePolicy.id) ?? null
        })
      : null,
    policies: rows.map((row) =>
      serializeTrainingPolicy(row, {
        updatedByName: latestAuditUsersByPolicy.get(row.id) ?? null
      })
    )
  };
}

export async function getTrainingPolicyByIdForOrganization({
  id,
  organizationId
}: {
  id: number;
  organizationId: string;
}) {
  const policy = await db.query.trainingPolicies.findFirst({
    where: and(eq(trainingPolicies.id, id), eq(trainingPolicies.organizationId, organizationId))
  });

  return policy ?? null;
}

export async function getTrainingPolicyDetailForOrganization({
  id,
  organizationId
}: {
  id: number;
  organizationId: string;
}) {
  const policy = await getTrainingPolicyByIdForOrganization({ id, organizationId });

  if (!policy) {
    return null;
  }

  const latestAuditUsers = await getLatestAuditUsersByPolicyIds([policy.id]);

  return serializeTrainingPolicy(policy, {
    updatedByName: latestAuditUsers.get(policy.id) ?? null
  });
}

export async function getTrainingPolicyByYearForOrganization({
  year,
  organizationId,
  excludeId
}: {
  year: number;
  organizationId: string;
  excludeId?: number;
}) {
  return (
    (await db.query.trainingPolicies.findFirst({
      where:
        excludeId === undefined
          ? and(eq(trainingPolicies.organizationId, organizationId), eq(trainingPolicies.year, year))
          : and(
              eq(trainingPolicies.organizationId, organizationId),
              eq(trainingPolicies.year, year),
              ne(trainingPolicies.id, excludeId)
            )
    })) ?? null
  );
}

export async function getResolvedTrainingPolicyForOrganization(organizationId: string) {
  const [activePolicy] = await db
    .select()
    .from(trainingPolicies)
    .where(
      and(
        eq(trainingPolicies.organizationId, organizationId),
        eq(trainingPolicies.isActive, true)
      )
    )
    .orderBy(desc(trainingPolicies.year), desc(trainingPolicies.updatedAt))
    .limit(1);

  if (activePolicy) {
    return serializeTrainingPolicy(activePolicy);
  }

  const [latestPolicy] = await db
    .select()
    .from(trainingPolicies)
    .where(eq(trainingPolicies.organizationId, organizationId))
    .orderBy(desc(trainingPolicies.year), desc(trainingPolicies.updatedAt))
    .limit(1);

  if (latestPolicy) {
    return serializeTrainingPolicy(latestPolicy);
  }

  return {
    id: 0,
    organization_id: organizationId,
    year: getDefaultTrainingPolicyYear(),
    total_hours: DEFAULT_TRAINING_POLICY.totalHours,
    k_hours: DEFAULT_TRAINING_POLICY.kHours,
    s_hours: DEFAULT_TRAINING_POLICY.sHours,
    a_hours: DEFAULT_TRAINING_POLICY.aHours,
    is_active: false,
    status: 'inactive' as const,
    display_name: buildTrainingPolicyDisplayName(getDefaultTrainingPolicyYear()),
    updated_by_name: null,
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString()
  };
}

export async function getNextTrainingPolicyYearForClone({
  organizationId,
  baseYear
}: {
  organizationId: string;
  baseYear: number;
}) {
  let nextYear = baseYear + 1;

  for (;;) {
    const existingPolicy = await getTrainingPolicyByYearForOrganization({
      year: nextYear,
      organizationId
    });

    if (!existingPolicy) {
      return nextYear;
    }

    nextYear += 1;
  }
}

export async function listTrainingPolicyHistoryForOrganization({
  organizationId,
  policyId
}: {
  organizationId: string;
  policyId: number;
}) {
  const rows = await db
    .select({
      id: auditLogs.id,
      action: auditLogs.action,
      entityName: auditLogs.entityName,
      entityId: auditLogs.entityId,
      oldValue: auditLogs.oldValue,
      newValue: auditLogs.newValue,
      userName: users.name,
      createdAt: auditLogs.createdAt
    })
    .from(auditLogs)
    .leftJoin(users, eq(auditLogs.userId, users.id))
    .where(
      and(
        eq(auditLogs.organizationId, organizationId),
        eq(auditLogs.moduleName, AUDIT_MODULE.TRAINING_POLICY),
        eq(auditLogs.entityId, String(policyId))
      )
    )
    .orderBy(desc(auditLogs.createdAt));

  return rows.map(
    (row): TrainingPolicyHistoryEntry => ({
      id: row.id,
      action: row.action,
      details: formatAuditDetails({
        id: row.id,
        organizationId,
        userId: null,
        userName: row.userName,
        userEmail: null,
        moduleName: AUDIT_MODULE.TRAINING_POLICY,
        action: row.action,
        entityName: row.entityName,
        entityId: row.entityId,
        oldValue: row.oldValue as Record<string, unknown> | null,
        newValue: row.newValue as Record<string, unknown> | null,
        ipAddress: null,
        userAgent: null,
        createdAt: row.createdAt
      }),
      user_name: row.userName ?? null,
      created_at: row.createdAt.toISOString(),
      old_value: row.oldValue as Record<string, unknown> | null,
      new_value: row.newValue as Record<string, unknown> | null
    })
  );
}

export function normalizeTrainingPolicyPayload(payload: TrainingPolicyMutationPayload) {
  return {
    year: payload.year,
    totalHours: normalizeNumber(payload.totalHours),
    kHours: normalizeNumber(payload.kHours),
    sHours: normalizeNumber(payload.sHours),
    aHours: normalizeNumber(payload.aHours),
    isActive: payload.isActive
  };
}
