import { BadRequestException, ConflictException, Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { InjectModel } from "@nestjs/mongoose";
import * as bcrypt from "bcryptjs";
import { Model, Types } from "mongoose";
import { User, UserDocument, UserRole } from "./schemas/user.schema";

export interface JwtPayload {
  sub: string;
  email: string;
  name: string;
  role: UserRole;
}

export interface PublicUser {
  id: string;
  email: string;
  name: string;
  role: UserRole;
  active: boolean;
  createdAt: string;
}

export interface CreateUserDto {
  email: string;
  name: string;
  role: UserRole;
  password: string;
  active?: boolean;
}

export interface UpdateUserDto {
  email?: string;
  name?: string;
  role?: UserRole;
  password?: string;
  active?: boolean;
}

const USER_ROLES: UserRole[] = ["store_keeper", "store_manager", "super_admin"];

@Injectable()
export class AuthService {
  constructor(
    @InjectModel(User.name) private readonly userModel: Model<UserDocument>,
    private readonly jwtService: JwtService,
  ) {}

  async login(email: string, password: string) {
    const user = await this.userModel.findOne({ email: email.toLowerCase().trim() }).exec();
    if (!user || !user.active) {
      throw new UnauthorizedException("Invalid credentials");
    }

    const valid = await bcrypt.compare(password, user.passwordHash);
    if (!valid) {
      throw new UnauthorizedException("Invalid credentials");
    }

    const payload: JwtPayload = {
      sub: (user._id as Types.ObjectId).toString(),
      email: user.email,
      name: user.name,
      role: user.role,
    };

    return {
      accessToken: this.jwtService.sign(payload),
      user: {
        id: payload.sub,
        email: user.email,
        name: user.name,
        role: user.role,
      },
    };
  }

  async findById(id: string) {
    const user = await this.userModel.findById(id).lean().exec();
    if (!user) return null;
    return this.toPublicUser(user);
  }

  async listUsers(): Promise<PublicUser[]> {
    const users = await this.userModel.find().sort({ name: 1, email: 1 }).lean().exec();
    return users.map((user) => this.toPublicUser(user));
  }

  async createUser(body: CreateUserDto): Promise<PublicUser> {
    this.validateUserPayload(body, true);

    const email = body.email.toLowerCase().trim();
    const existing = await this.userModel.findOne({ email }).lean().exec();
    if (existing) {
      throw new ConflictException("A user with this email already exists");
    }

    const created = await this.userModel.create({
      email,
      name: body.name.trim(),
      role: body.role,
      active: body.active ?? true,
      passwordHash: await bcrypt.hash(body.password, 10),
      createdAt: new Date().toISOString(),
    });

    return this.toPublicUser(created);
  }

  async updateUser(id: string, body: UpdateUserDto): Promise<PublicUser> {
    if (!Types.ObjectId.isValid(id)) {
      throw new NotFoundException("User not found");
    }
    this.validateUserPayload(body, false);

    const updates: Partial<User> = {};
    if (body.email !== undefined) {
      const email = body.email.toLowerCase().trim();
      const existing = await this.userModel.findOne({ email, _id: { $ne: id } }).lean().exec();
      if (existing) {
        throw new ConflictException("A user with this email already exists");
      }
      updates.email = email;
    }
    if (body.name !== undefined) updates.name = body.name.trim();
    if (body.role !== undefined) updates.role = body.role;
    if (body.active !== undefined) updates.active = body.active;
    if (body.password !== undefined && body.password.trim()) {
      updates.passwordHash = await bcrypt.hash(body.password, 10);
    }

    const user = await this.userModel.findByIdAndUpdate(id, { $set: updates }, { new: true }).exec();
    if (!user) {
      throw new NotFoundException("User not found");
    }
    return this.toPublicUser(user);
  }

  async deactivateUser(id: string): Promise<PublicUser> {
    if (!Types.ObjectId.isValid(id)) {
      throw new NotFoundException("User not found");
    }
    const user = await this.userModel.findByIdAndUpdate(id, { $set: { active: false } }, { new: true }).exec();
    if (!user) {
      throw new NotFoundException("User not found");
    }
    return this.toPublicUser(user);
  }

  async seedDefaultUsers() {
    const count = await this.userModel.countDocuments().exec();
    if (count > 0) return;

    const hash = async (pw: string) => bcrypt.hash(pw, 10);

    await this.userModel.insertMany([
      {
        email: "keeper@silal.ae",
        passwordHash: await hash("keeper123"),
        name: "Store Keeper",
        role: "store_keeper" as UserRole,
        active: true,
        createdAt: new Date().toISOString(),
      },
      {
        email: "manager@silal.ae",
        passwordHash: await hash("manager123"),
        name: "Store Manager",
        role: "store_manager" as UserRole,
        active: true,
        createdAt: new Date().toISOString(),
      },
      {
        email: "admin@silal.ae",
        passwordHash: await hash("admin123"),
        name: "Super Admin",
        role: "super_admin" as UserRole,
        active: true,
        createdAt: new Date().toISOString(),
      },
    ]);
  }

  private validateUserPayload(body: Partial<CreateUserDto>, requirePassword: boolean) {
    if (body.email !== undefined && !body.email.trim()) {
      throw new BadRequestException("Email is required");
    }
    if (body.name !== undefined && !body.name.trim()) {
      throw new BadRequestException("Name is required");
    }
    if (body.role !== undefined && !USER_ROLES.includes(body.role)) {
      throw new BadRequestException("Invalid user role");
    }
    if (requirePassword && !body.password?.trim()) {
      throw new BadRequestException("Password is required");
    }
    if (body.password !== undefined && body.password.trim() && body.password.length < 6) {
      throw new BadRequestException("Password must be at least 6 characters");
    }
  }

  private toPublicUser(user: Pick<User, "email" | "name" | "role" | "active" | "createdAt"> & { _id?: unknown; id?: unknown }): PublicUser {
    const id = user._id instanceof Types.ObjectId ? user._id.toString() : String(user._id ?? user.id ?? "");
    return {
      id,
      email: user.email,
      name: user.name,
      role: user.role,
      active: user.active,
      createdAt: user.createdAt,
    };
  }
}
