import {
  Injectable,
  Logger,
  ForbiddenException,
  UnauthorizedException,
  ConflictException,
  InternalServerErrorException,
} from "@nestjs/common";
import { WorkspaceMemberRole } from "@prisma/client";
import { hash, compare } from "bcryptjs";
import { randomBytes, timingSafeEqual } from "crypto";
import { PrismaService } from "../../prisma/prisma.service";

const BCRYPT_ROUNDS = 12;

/** Session expiry: 7 days (matches BetterAuth config in auth.config.ts) */
const SESSION_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;

interface SetupResult {
  user: {
    id: string;
    email: string;
    name: string;
    isLocalAuth: boolean;
    createdAt: Date;
  };
  session: {
    token: string;
    expiresAt: Date;
  };
}

interface LoginResult {
  user: {
    id: string;
    email: string;
    name: string;
  };
  session: {
    token: string;
    expiresAt: Date;
  };
}

@Injectable()
export class LocalAuthService {
  private readonly logger = new Logger(LocalAuthService.name);

  constructor(private readonly prisma: PrismaService) {}

  /**
   * First-time break-glass user creation.
   * Validates the setup token, creates a local auth user with bcrypt-hashed password,
   * and assigns OWNER role on the default workspace.
   */
  async setup(
    email: string,
    name: string,
    password: string,
    setupToken: string,
    ipAddress?: string,
    userAgent?: string
  ): Promise<SetupResult> {
    this.validateSetupToken(setupToken);

    const existing = await this.prisma.user.findUnique({ where: { email } });
    if (existing) {
      throw new ConflictException("A user with this email already exists");
    }

    const passwordHash = await hash(password, BCRYPT_ROUNDS);

    const result = await this.prisma.$transaction(async (tx) => {
      const user = await tx.user.create({
        data: {
          email,
          name,
          isLocalAuth: true,
          passwordHash,
          emailVerified: true,
        },
        select: {
          id: true,
          email: true,
          name: true,
          isLocalAuth: true,
          createdAt: true,
        },
      });

      // Find or create a default workspace and assign OWNER role
      await this.assignDefaultWorkspace(tx, user.id);

      // Create a BetterAuth-compatible session
      const session = await this.createSession(tx, user.id, ipAddress, userAgent);

      return { user, session };
    });

    this.logger.log(`Break-glass user created: ${email}`);
    return result;
  }

  /**
   * Break-glass login: verify email + password against bcrypt hash.
   * Only works for users with isLocalAuth=true.
   */
  async login(
    email: string,
    password: string,
    ipAddress?: string,
    userAgent?: string
  ): Promise<LoginResult> {
    const user = await this.prisma.user.findUnique({
      where: { email },
      select: {
        id: true,
        email: true,
        name: true,
        isLocalAuth: true,
        passwordHash: true,
        deactivatedAt: true,
      },
    });

    if (!user?.isLocalAuth) {
      throw new UnauthorizedException("Invalid email or password");
    }

    if (user.deactivatedAt) {
      throw new UnauthorizedException("Account has been deactivated");
    }

    if (!user.passwordHash) {
      this.logger.error(`Local auth user ${email} has no password hash`);
      throw new InternalServerErrorException("Account configuration error");
    }

    const passwordValid = await compare(password, user.passwordHash);
    if (!passwordValid) {
      throw new UnauthorizedException("Invalid email or password");
    }

    const session = await this.createSession(this.prisma, user.id, ipAddress, userAgent);

    this.logger.log(`Break-glass login: ${email}`);
    return {
      user: { id: user.id, email: user.email, name: user.name },
      session,
    };
  }

  /**
   * Validate the setup token against the environment variable.
   */
  private validateSetupToken(token: string): void {
    const expectedToken = process.env.BREAKGLASS_SETUP_TOKEN;

    if (!expectedToken || expectedToken.trim() === "") {
      throw new ForbiddenException(
        "Break-glass setup is not configured. Set BREAKGLASS_SETUP_TOKEN environment variable."
      );
    }

    const tokenBuffer = Buffer.from(token);
    const expectedBuffer = Buffer.from(expectedToken);
    if (
      tokenBuffer.length !== expectedBuffer.length ||
      !timingSafeEqual(tokenBuffer, expectedBuffer)
    ) {
      this.logger.warn("Invalid break-glass setup token attempt");
      throw new ForbiddenException("Invalid setup token");
    }
  }

  /**
   * Find the first workspace or create a default one, then assign OWNER role.
   */
  private async assignDefaultWorkspace(
    tx: Parameters<Parameters<PrismaService["$transaction"]>[0]>[0],
    userId: string
  ): Promise<void> {
    let workspace = await tx.workspace.findFirst({
      orderBy: { createdAt: "asc" },
      select: { id: true },
    });

    workspace ??= await tx.workspace.create({
      data: {
        name: "Default Workspace",
        ownerId: userId,
        settings: {},
      },
      select: { id: true },
    });

    await tx.workspaceMember.create({
      data: {
        workspaceId: workspace.id,
        userId,
        role: WorkspaceMemberRole.OWNER,
      },
    });
  }

  /**
   * Create a BetterAuth-compatible session record.
   */
  private async createSession(
    tx: { session: { create: typeof PrismaService.prototype.session.create } },
    userId: string,
    ipAddress?: string,
    userAgent?: string
  ): Promise<{ token: string; expiresAt: Date }> {
    const token = randomBytes(32).toString("hex");
    const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);

    await tx.session.create({
      data: {
        userId,
        token,
        expiresAt,
        ipAddress: ipAddress ?? null,
        userAgent: userAgent ?? null,
      },
    });

    return { token, expiresAt };
  }
}
