import { compare } from 'bcryptjs';
import { and, eq, isNull, or } from 'drizzle-orm';
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
import { memberships, organizations, users } from '@/db/schema';
import {
  AUDIT_ACTION,
  AUDIT_MODULE,
  getRequestMetadata,
  logAuditEvent,
  toAuditValue
} from '@/features/audit-logs/server/audit-service';
import { syncUserEmployeeLink } from '@/features/employees/server/employee-identity-linking';
import {
  getBusinessRole,
  getDefaultPermissionsForRole,
  isHRD,
  isSuperAdmin,
  normalizeOrganizationRole
} from '@/lib/auth/roles';
import { resolveEffectivePermissions } from '@/lib/auth/authorization.server';
import { db } from '@/lib/db';

const signInSchema = z.object({
  identifier: z.string().trim().min(3),
  password: z.string().min(8)
});

const authSecret =
  process.env.AUTH_SECRET ??
  process.env.NEXTAUTH_SECRET ??
  (process.env.NODE_ENV !== 'production' ? 'dev-only-auth-secret-change-me' : undefined);

export const { handlers, auth, signIn, signOut } = NextAuth({
  trustHost: true,
  secret: authSecret,
  pages: {
    signIn: '/auth/sign-in'
  },
  session: {
    strategy: 'jwt'
  },
  providers: [
    Credentials({
      credentials: {
        identifier: { label: 'Username or Email', type: 'text' },
        password: { label: 'Password', type: 'password' }
      },
      authorize: async (credentials, request) => {
        const { ipAddress, userAgent } = getRequestMetadata(request);
        const parsed = signInSchema.safeParse(credentials);

        if (!parsed.success) {
          await logAuditEvent({
            module: AUDIT_MODULE.AUTHENTICATION,
            action: AUDIT_ACTION.LOGIN_FAILED,
            newValue: toAuditValue({
              identifier:
                typeof credentials?.identifier === 'string' ? credentials.identifier : null,
              reason: 'VALIDATION_FAILED'
            }),
            ipAddress,
            userAgent
          });
          return null;
        }

        const identifier = parsed.data.identifier.trim().toLowerCase();
        const [user] = await db
          .select()
          .from(users)
          .where(or(eq(users.email, identifier), eq(users.username, identifier)))
          .limit(1);

        if (!user || !user.isActive || !user.passwordHash) {
          await logAuditEvent({
            organizationId: user?.activeOrganizationId ?? null,
            userId: user?.id ?? null,
            module: AUDIT_MODULE.AUTHENTICATION,
            action: AUDIT_ACTION.LOGIN_FAILED,
            newValue: toAuditValue({
              identifier,
              reason: !user
                ? 'USER_NOT_FOUND'
                : !user.isActive
                  ? 'USER_INACTIVE'
                  : 'PASSWORD_NOT_SET'
            }),
            ipAddress,
            userAgent
          });
          return null;
        }

        const isValid = await compare(parsed.data.password, user.passwordHash);

        if (!isValid) {
          await logAuditEvent({
            organizationId: user.activeOrganizationId,
            userId: user.id,
            module: AUDIT_MODULE.AUTHENTICATION,
            action: AUDIT_ACTION.LOGIN_FAILED,
            newValue: toAuditValue({
              email: user.email,
              identifier,
              reason: 'INVALID_PASSWORD'
            }),
            ipAddress,
            userAgent
          });
          return null;
        }

        await db
          .update(users)
          .set({
            provider: 'credentials',
            lastLoginAt: new Date(),
            updatedAt: new Date()
          })
          .where(eq(users.id, user.id));

        const linkedEmployee =
          user.activeOrganizationId && user.employeeCode
            ? await syncUserEmployeeLink({
                organizationId: user.activeOrganizationId,
                userId: user.id,
                employeeCode: user.employeeCode
              })
            : null;

        await logAuditEvent({
          organizationId: user.activeOrganizationId,
          userId: user.id,
          module: AUDIT_MODULE.AUTHENTICATION,
          action: AUDIT_ACTION.LOGIN_SUCCESS,
          newValue: toAuditValue({
            email: user.email,
            identifier
          }),
          ipAddress,
          userAgent
        });

        return {
          id: user.id,
          name: user.name,
          email: user.email,
          username: user.username,
          image: user.image,
          systemRole: user.systemRole,
          activeOrganizationId: user.activeOrganizationId,
          employeeCode: user.employeeCode,
          employeeId: linkedEmployee?.id ?? user.employeeId ?? null,
          provider: 'credentials'
        };
      }
    })
  ],
  callbacks: {
    jwt: async ({ token, user }) => {
      if (user) {
        token.sub = user.id;
      }

      return token;
    },
    session: async ({ session, token }) => {
      if (!token.sub || !session.user) {
        return session;
      }

      const [dbUser] = await db.select().from(users).where(eq(users.id, token.sub)).limit(1);

      if (!dbUser) {
        return session;
      }

      const adminMembership = await db.query.memberships.findFirst({
        where: eq(memberships.userId, dbUser.id),
        columns: {
          role: true
        }
      });

      const canAccessAllOrganizations =
        isSuperAdmin(dbUser.systemRole) ||
        isHRD({
          systemRole: dbUser.systemRole,
          membershipRole: adminMembership?.role ?? null
        });

      const rows = canAccessAllOrganizations
        ? await db
            .select({
              organizationId: organizations.id,
              membershipRole: memberships.role,
              permissions: memberships.permissions,
              name: organizations.name,
              slug: organizations.slug,
              plan: organizations.plan,
              imageUrl: organizations.imageUrl
            })
            .from(organizations)
            .leftJoin(
              memberships,
              and(
                eq(memberships.organizationId, organizations.id),
                eq(memberships.userId, dbUser.id)
              )
            )
            .where(and(eq(organizations.isActive, true), isNull(organizations.deletedAt)))
        : await db
            .select({
              organizationId: memberships.organizationId,
              membershipRole: memberships.role,
              permissions: memberships.permissions,
              name: organizations.name,
              slug: organizations.slug,
              plan: organizations.plan,
              imageUrl: organizations.imageUrl
            })
            .from(memberships)
            .innerJoin(organizations, eq(organizations.id, memberships.organizationId))
            .where(
              and(
                eq(memberships.userId, dbUser.id),
                eq(organizations.isActive, true),
                isNull(organizations.deletedAt)
              )
            );

      const scopedRows = rows
        .filter((row) => canAccessAllOrganizations || row.membershipRole)
        .map((row) => {
          const membershipRole = row.membershipRole ?? 'admin';

          return {
            organizationId: row.organizationId,
            membershipRole,
            role: normalizeOrganizationRole(membershipRole) ?? 'user',
            fallbackPermissions:
              row.permissions && row.permissions.length > 0
                ? row.permissions
                : getDefaultPermissionsForRole(membershipRole),
            name: row.name,
            slug: row.slug,
            plan: row.plan,
            imageUrl: row.imageUrl
          };
        });

      const scopedRowsWithPermissions = await Promise.all(
        scopedRows.map(async (row) => {
          const effectivePermissions = await resolveEffectivePermissions({
            userId: dbUser.id,
            organizationId: row.organizationId,
            fallbackPermissions: row.fallbackPermissions,
            fallbackRole: row.membershipRole
          });

          return {
            ...row,
            permissions: effectivePermissions.permissions,
            effectivePermissions: effectivePermissions.permissions,
            permissionTemplateIds: effectivePermissions.permissionTemplateIds
          };
        })
      );

      const activeMembership =
        scopedRowsWithPermissions.find(
          (row) => row.organizationId === dbUser.activeOrganizationId
        ) ??
        scopedRowsWithPermissions[0] ??
        null;

      session.user.id = dbUser.id;
      session.user.name = dbUser.name;
      session.user.email = dbUser.email;
      session.user.image = dbUser.image;
      session.user.systemRole = dbUser.systemRole;
      session.user.employeeCode = dbUser.employeeCode;
      session.user.employeeId = dbUser.employeeId;
      session.user.provider = dbUser.provider;
      session.user.activeOrganizationId = activeMembership?.organizationId ?? null;
      session.user.activeOrganizationName = activeMembership?.name ?? null;
      session.user.activeOrganizationPlan = activeMembership?.plan ?? null;
      session.user.membershipRole = activeMembership?.membershipRole ?? null;
      session.user.role = activeMembership?.role ?? null;
      session.user.businessRole = getBusinessRole({
        systemRole: dbUser.systemRole,
        membershipRole: activeMembership?.membershipRole ?? null,
        role: activeMembership?.role ?? null
      });
      session.user.permissions = activeMembership?.permissions ?? [];
      session.user.effectivePermissions = activeMembership?.effectivePermissions ?? [];
      session.user.permissionTemplateIds = activeMembership?.permissionTemplateIds ?? [];
      session.user.organizations = scopedRowsWithPermissions.map((row) => ({
        id: row.organizationId,
        name: row.name,
        slug: row.slug,
        membershipRole: row.membershipRole,
        role: row.role,
        plan: row.plan,
        imageUrl: row.imageUrl,
        permissionTemplateIds: row.permissionTemplateIds
      }));

      // Keep the persisted active organization aligned with the filtered session scope.
      if (
        (!dbUser.activeOrganizationId && activeMembership) ||
        (dbUser.activeOrganizationId !== activeMembership?.organizationId && activeMembership) ||
        (dbUser.activeOrganizationId && !activeMembership)
      ) {
        await db
          .update(users)
          .set({ activeOrganizationId: activeMembership?.organizationId ?? null })
          .where(eq(users.id, dbUser.id));
      }

      return session;
    }
  }
});
