import { hash } from 'bcryptjs';
import { and, asc, count, desc, eq, ilike, isNull, or, type SQL } from 'drizzle-orm';
import { departments, memberships, organizations, positions, users } from '@/db/schema';
import { syncUserEmployeeLink } from '@/features/employees/server/employee-identity-linking';
import {
  canAssignAdminRole,
  canManageUsers,
  getDefaultPermissionsForRole,
  isSuperAdmin,
  normalizeOrganizationRole
} from '@/lib/auth/roles';
import { db } from '@/lib/db';
import type { UserFilters, UserMasterOptionsResponse, UserMutationPayload } from '../api/types';

type AccessContext = {
  currentUserId: string;
  currentOrganizationId: string | null;
  membershipRole: string | null;
  businessRole: 'IT' | 'HRD' | 'EMPLOYEE' | null;
  systemRole: string;
};

type UserRow = {
  id: string;
  name: string;
  username: string | null;
  employeeCode: string | null;
  email: string;
  departmentId: number | null;
  departmentName: string | null;
  positionId: number | null;
  positionName: string | null;
  companyName: string | null;
  hiredAt: Date | null;
  systemRole: string;
  isActive: boolean;
  organizationId: string;
  organizationName: string;
  membershipRole: string;
  createdAt: Date;
  updatedAt: Date;
};

const sortMap = {
  full_name: users.name,
  username: users.username,
  employee_code: users.employeeCode,
  email: users.email,
  department_name: departments.name,
  position_name: positions.name,
  role: memberships.role,
  updated_at: users.updatedAt
} as const;

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

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

function toMembershipRole(role: 'admin' | 'user') {
  return role === 'admin' ? 'admin' : 'member';
}

function toStatusLabel(isActive: boolean) {
  return isActive ? 'Active' : 'Inactive';
}

export function serializeUser(row: UserRow) {
  return {
    id: row.id,
    full_name: row.name,
    username: row.username,
    employee_code: row.employeeCode,
    email: row.email,
    department_id: row.departmentId,
    department_name: row.departmentName,
    position_id: row.positionId,
    position_name: row.positionName,
    company_name: row.companyName,
    hired_at: row.hiredAt ? row.hiredAt.toISOString() : null,
    organization_id: row.organizationId,
    organization_name: row.organizationName,
    organization_role: normalizeOrganizationRole(row.membershipRole),
    status: toStatusLabel(row.isActive),
    created_at: row.createdAt.toISOString(),
    updated_at: row.updatedAt.toISOString()
  };
}

export function assertCanManageUsers(access: AccessContext) {
  if (
    !canManageUsers({
      businessRole: access.businessRole,
      systemRole: access.systemRole,
      membershipRole: access.membershipRole
    })
  ) {
    throw new Error('FORBIDDEN');
  }
}

export function getTargetOrganizationId(payload: UserMutationPayload, access: AccessContext) {
  return payload.organization_id?.trim() || access.currentOrganizationId;
}

function getBaseUserQuery() {
  return db
    .select({
      id: users.id,
      name: users.name,
      username: users.username,
      employeeCode: users.employeeCode,
      email: users.email,
      departmentId: users.departmentId,
      departmentName: departments.name,
      positionId: users.positionId,
      positionName: positions.name,
      companyName: users.companyName,
      hiredAt: users.hiredAt,
      systemRole: users.systemRole,
      isActive: users.isActive,
      organizationId: organizations.id,
      organizationName: organizations.name,
      membershipRole: memberships.role,
      createdAt: users.createdAt,
      updatedAt: users.updatedAt
    })
    .from(memberships)
    .innerJoin(users, eq(users.id, memberships.userId))
    .innerJoin(organizations, eq(organizations.id, memberships.organizationId))
    .leftJoin(departments, eq(users.departmentId, departments.id))
    .leftJoin(positions, eq(users.positionId, positions.id));
}

export async function listUsersForAccess({
  filters,
  access
}: {
  filters: UserFilters;
  access: AccessContext;
}) {
  assertCanManageUsers(access);

  const rawPage = Number(filters.page ?? 1);
  const rawLimit = Number(filters.limit ?? 10);
  const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;
  const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 10;
  const search = filters.search?.trim();
  const roles = filters.roles
    ? filters.roles
        .split(/[.,]/)
        .map((value) => value.trim().toLowerCase())
        .filter(Boolean)
    : [];
  const organizationId = access.currentOrganizationId;
  const roleClauses: SQL[] = [];

  for (const role of roles) {
    if (role === 'admin') {
      roleClauses.push(eq(memberships.role, 'admin'));
    } else if (role === 'user') {
      roleClauses.push(eq(memberships.role, 'member'));
    }
  }

  const clauses: SQL[] = [
    ...(organizationId ? [eq(memberships.organizationId, organizationId)] : []),
    ...roleClauses,
    ...(search
      ? [
          or(
            ilike(users.name, `%${search}%`),
            ilike(users.username, `%${search}%`),
            ilike(users.email, `%${search}%`),
            ilike(users.employeeCode, `%${search}%`),
            ilike(departments.name, `%${search}%`),
            ilike(positions.name, `%${search}%`)
          )!
        ]
      : [])
  ];

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

  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(users.createdAt);
    }
  }

  const offset = (page - 1) * limit;

  const filteredBaseQuery = where ? getBaseUserQuery().where(where) : getBaseUserQuery();
  const countSubquery = filteredBaseQuery.as('scoped_users');
  const [totalResult] = await db.select({ value: count() }).from(countSubquery);
  const rows = await filteredBaseQuery.orderBy(orderByClause).limit(limit).offset(offset);

  return {
    offset,
    limit,
    total: totalResult?.value ?? 0,
    users: rows.map((row) => serializeUser(row as UserRow))
  };
}

export async function getUserForAccess({ id, access }: { id: string; access: AccessContext }) {
  assertCanManageUsers(access);

  const organizationId = access.currentOrganizationId;
  const clauses: SQL[] = [
    eq(users.id, id),
    ...(organizationId ? [eq(memberships.organizationId, organizationId)] : [])
  ];
  const where = clauses.length > 1 ? and(...clauses) : clauses[0];
  const [row] = await getBaseUserQuery().where(where).limit(1);
  return row ? serializeUser(row as UserRow) : null;
}

export async function getUserMasterOptionsForAccess({
  access
}: {
  access: AccessContext;
}): Promise<UserMasterOptionsResponse> {
  const organizationId = access.currentOrganizationId;

  if (!organizationId) {
    return {
      departments: [],
      positions: [],
      users: []
    };
  }

  const [departmentRows, positionRows, userRows] = 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({
        id: users.id,
        name: users.name,
        employeeCode: users.employeeCode
      })
      .from(memberships)
      .innerJoin(users, eq(users.id, memberships.userId))
      .where(and(eq(memberships.organizationId, organizationId), eq(users.isActive, true)))
      .orderBy(users.name)
  ]);

  return {
    departments: departmentRows.map((department) => ({
      value: String(department.id),
      label: department.name
    })),
    positions: positionRows.map((position) => ({
      value: String(position.id),
      label: position.name
    })),
    users: userRows
      .filter((user) =>
        canManageUsers({
          businessRole: access.businessRole,
          systemRole: access.systemRole,
          membershipRole: access.membershipRole
        })
          ? true
          : user.id === access.currentUserId
      )
      .map((user) => ({
        value: user.id,
        label: `${user.name}${user.employeeCode ? ` (${user.employeeCode})` : ''}`
      }))
  };
}

export async function upsertUserForAccess({
  payload,
  access
}: {
  payload: UserMutationPayload;
  access: AccessContext;
}) {
  assertCanManageUsers(access);

  const targetOrganizationId = getTargetOrganizationId(payload, access);

  if (!targetOrganizationId) {
    throw new Error('ORGANIZATION_REQUIRED');
  }

  if (!isSuperAdmin(access.systemRole) && targetOrganizationId !== access.currentOrganizationId) {
    throw new Error('FORBIDDEN');
  }

  if (payload.organization_role === 'admin' && !canAssignAdminRole(access.systemRole)) {
    throw new Error('ADMIN_ROLE_FORBIDDEN');
  }

  const email = payload.email.trim().toLowerCase();
  const username = normalizeUsername(payload.username);
  const [existingUser] = await db.select().from(users).where(eq(users.email, email)).limit(1);

  if (username) {
    const [usernameOwner] = await db
      .select()
      .from(users)
      .where(eq(users.username, username))
      .limit(1);

    if (usernameOwner && usernameOwner.id !== existingUser?.id) {
      throw new Error('USERNAME_EXISTS');
    }
  }

  const targetOrganization = await db.query.organizations.findFirst({
    where: and(
      eq(organizations.id, targetOrganizationId),
      eq(organizations.isActive, true),
      isNull(organizations.deletedAt)
    )
  });

  if (!targetOrganization) {
    throw new Error('ORGANIZATION_REQUIRED');
  }

  const membershipRole = toMembershipRole(payload.organization_role);
  const companyName = targetOrganization.name;
  const departmentId = payload.department_id ? Number(payload.department_id) : null;
  const positionId = payload.position_id ? Number(payload.position_id) : null;
  const employeeCode = normalizeOptionalText(payload.employee_code);

  if (existingUser) {
    const existingMembership = await db.query.memberships.findFirst({
      where: and(
        eq(memberships.userId, existingUser.id),
        eq(memberships.organizationId, targetOrganizationId)
      )
    });

    const updateValues: Partial<typeof users.$inferInsert> = {
      name: payload.full_name.trim(),
      email,
      username,
      employeeCode,
      departmentId,
      positionId,
      companyName,
      isActive: payload.is_active,
      updatedAt: new Date()
    };

    if (payload.password?.trim()) {
      updateValues.passwordHash = await hash(payload.password.trim(), 10);
    }

    await db.update(users).set(updateValues).where(eq(users.id, existingUser.id));

    if (existingMembership) {
      await db
        .update(memberships)
        .set({
          role: membershipRole,
          permissions: getDefaultPermissionsForRole(membershipRole),
          updatedAt: new Date()
        })
        .where(eq(memberships.id, existingMembership.id));
    } else {
      await db.insert(memberships).values({
        id: crypto.randomUUID(),
        userId: existingUser.id,
        organizationId: targetOrganizationId,
        role: membershipRole,
        permissions: getDefaultPermissionsForRole(membershipRole)
      });
    }

    if (!existingUser.activeOrganizationId) {
      await db
        .update(users)
        .set({ activeOrganizationId: targetOrganizationId })
        .where(eq(users.id, existingUser.id));
    }

    await syncUserEmployeeLink({
      organizationId: targetOrganizationId,
      userId: existingUser.id,
      employeeCode
    });

    return existingUser.id;
  }

  if (!payload.password?.trim() || payload.password.trim().length < 8) {
    throw new Error('PASSWORD_REQUIRED');
  }

  const userId = crypto.randomUUID();

  await db.insert(users).values({
    id: userId,
    name: payload.full_name.trim(),
    email,
    username,
    passwordHash: await hash(payload.password.trim(), 10),
    systemRole: 'standard',
    isActive: payload.is_active,
    employeeCode,
    departmentId,
    positionId,
    companyName,
    activeOrganizationId: targetOrganizationId
  });

  await db.insert(memberships).values({
    id: crypto.randomUUID(),
    userId,
    organizationId: targetOrganizationId,
    role: membershipRole,
    permissions: getDefaultPermissionsForRole(membershipRole)
  });

  await syncUserEmployeeLink({
    organizationId: targetOrganizationId,
    userId,
    employeeCode
  });

  return userId;
}

export async function updateUserForAccess({
  userId,
  payload,
  access
}: {
  userId: string;
  payload: UserMutationPayload;
  access: AccessContext;
}) {
  assertCanManageUsers(access);

  const targetOrganizationId = getTargetOrganizationId(payload, access);

  if (!targetOrganizationId) {
    throw new Error('ORGANIZATION_REQUIRED');
  }

  if (!isSuperAdmin(access.systemRole) && targetOrganizationId !== access.currentOrganizationId) {
    throw new Error('FORBIDDEN');
  }

  if (payload.organization_role === 'admin' && !canAssignAdminRole(access.systemRole)) {
    throw new Error('ADMIN_ROLE_FORBIDDEN');
  }

  const membership = await db.query.memberships.findFirst({
    where: and(eq(memberships.userId, userId), eq(memberships.organizationId, targetOrganizationId))
  });

  if (!membership) {
    throw new Error('NOT_FOUND');
  }

  const targetOrganization = await db.query.organizations.findFirst({
    where: and(
      eq(organizations.id, targetOrganizationId),
      eq(organizations.isActive, true),
      isNull(organizations.deletedAt)
    )
  });

  if (!targetOrganization) {
    throw new Error('ORGANIZATION_REQUIRED');
  }

  const username = normalizeUsername(payload.username);

  if (username) {
    const [usernameOwner] = await db
      .select()
      .from(users)
      .where(eq(users.username, username))
      .limit(1);

    if (usernameOwner && usernameOwner.id !== userId) {
      throw new Error('USERNAME_EXISTS');
    }
  }

  const updateValues: Partial<typeof users.$inferInsert> = {
    name: payload.full_name.trim(),
    email: payload.email.trim().toLowerCase(),
    username,
    employeeCode: normalizeOptionalText(payload.employee_code),
    departmentId: payload.department_id ? Number(payload.department_id) : null,
    positionId: payload.position_id ? Number(payload.position_id) : null,
    companyName: targetOrganization.name,
    isActive: payload.is_active,
    updatedAt: new Date()
  };

  if (payload.password?.trim()) {
    updateValues.passwordHash = await hash(payload.password.trim(), 10);
  }

  await db.update(users).set(updateValues).where(eq(users.id, userId));
  await db
    .update(memberships)
    .set({
      role: toMembershipRole(payload.organization_role),
      permissions: getDefaultPermissionsForRole(toMembershipRole(payload.organization_role)),
      updatedAt: new Date()
    })
    .where(eq(memberships.id, membership.id));

  await syncUserEmployeeLink({
    organizationId: targetOrganizationId,
    userId,
    employeeCode: updateValues.employeeCode
  });
}

export async function deactivateUserForAccess({
  userId,
  access
}: {
  userId: string;
  access: AccessContext;
}) {
  assertCanManageUsers(access);

  const organizationId = access.currentOrganizationId;

  if (organizationId) {
    const membership = await db.query.memberships.findFirst({
      where: and(eq(memberships.userId, userId), eq(memberships.organizationId, organizationId))
    });

    if (!membership) {
      throw new Error('NOT_FOUND');
    }
  }

  await db
    .update(users)
    .set({
      isActive: false,
      updatedAt: new Date()
    })
    .where(eq(users.id, userId));
}
