/**
 * AuthService — логика аутентификации ИНФУД.
 *
 * Полностью сохраняет поведение infud-server-v4.js:
 *  - bcrypt-верификация пароля
 *  - JWT с jti (UUID сессии) для управления активными сессиями
 *  - lockout после N неудачных попыток
 *  - IP-scoring (scoreIP) через SecurityStoreService
 *  - Аудит каждого входа/выхода
 */
import {
  Injectable, UnauthorizedException, ForbiddenException,
  BadRequestException, NotFoundException, Logger, OnModuleInit,
} from '@nestjs/common';
import { JwtService }          from '@nestjs/jwt';
import * as bcrypt             from 'bcrypt';
import * as crypto             from 'crypto';
import * as fs                 from 'fs';
import * as path               from 'path';

import { AppConfigService }    from '../config/app-config.service';
import { ROLE_LEVELS, UserRole } from '../../common/guards/role.guard';

const SALT_ROUNDS = 12;

/** Пользователь в JSON-хранилище */
interface UserRecord {
  id:              string;
  login:           string;
  passHash:        string;
  name:            string;
  role:            string;
  dept:            string;
  ext:             string;
  email:           string;
  sipExt:          string | null;
  active:          boolean;
  loginAttempts:   number;
  lockedUntil:     string | null;
  twoFactorEnabled:boolean;
  createdAt:       string;
  lastLogin:       string | null;
  lastIp:          string | null;
}

/** Активная сессия (хранится in-memory) */
export interface ActiveSession {
  userId:  string;
  login:   string;
  name:    string;
  role:    string;
  ip:      string;
  loginAt: string;
  lastSeen:string;
  ua:      string;
}

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

  /** Активные сессии: Map<jti, session> */
  private readonly sessions = new Map<string, ActiveSession>();

  /** IP-scoring: Map<ip, {score, ts}> */
  private readonly ipScores = new Map<string, { score: number; ts: number }>();
  private readonly ipBans   = new Map<string, number>(); // ip → bannedUntil

  constructor(
    private readonly jwt:    JwtService,
    private readonly appCfg: AppConfigService,
  ) {}

  onModuleInit(): void {
    this.ensureDefaultUsers().catch(e =>
      this.logger.error('Ошибка создания дефолтных пользователей:', e.message)
    );
    // Чистим просроченные сессии каждый час
    setInterval(() => this.cleanupSessions(), 3_600_000);
  }

  // ─────────────────────────────────────────────────────────────
  // PUBLIC API
  // ─────────────────────────────────────────────────────────────

  async login(loginOrEmail: string, password: string, ip: string, ua: string) {
    // IP-блокировка
    if (this.isBanned(ip)) throw new ForbiddenException('IP временно заблокирован');

    const users = this.readUsers();
    const user  = users.find(
      u => u.login === loginOrEmail ||
           (u.email && u.email.toLowerCase() === loginOrEmail.toLowerCase())
    );

    if (!user) {
      this.scoreIP(ip, 20);
      await this.audit(null, loginOrEmail, 'LOGIN_FAIL', 'Not found', ip);
      throw new UnauthorizedException('Неверный логин или пароль');
    }

    if (!user.active) throw new ForbiddenException('Аккаунт заблокирован');

    if (user.lockedUntil && new Date() < new Date(user.lockedUntil)) {
      const min = Math.ceil((new Date(user.lockedUntil).getTime() - Date.now()) / 60_000);
      throw new ForbiddenException(`Аккаунт заблокирован на ${min} мин.`);
    }

    const maxAttempts = Number(this.appCfg.get('maxAttempts') ?? 5);
    const lockoutMs   = Number(this.appCfg.get('lockoutMin')  ?? 5) * 60_000;
    const ok          = await bcrypt.compare(password, user.passHash);

    if (!ok) {
      user.loginAttempts = (user.loginAttempts ?? 0) + 1;
      if (user.loginAttempts >= maxAttempts) {
        user.lockedUntil   = new Date(Date.now() + lockoutMs).toISOString();
        user.loginAttempts = 0;
      }
      await this.writeUsers(users);
      this.scoreIP(ip, 15);
      await this.audit(user.id, user.login, 'LOGIN_FAIL', `attempt ${user.loginAttempts}`, ip);
      throw new UnauthorizedException('Неверный логин или пароль');
    }

    // Успех
    const jti   = crypto.randomBytes(16).toString('hex');
    const token = this.jwt.sign({ id: user.id, login: user.login, role: user.role, name: user.name, jti });

    user.loginAttempts = 0;
    user.lockedUntil   = null;
    user.lastLogin     = new Date().toISOString();
    user.lastIp        = ip;
    await this.writeUsers(users);

    this.sessions.set(jti, {
      userId: user.id, login: user.login, name: user.name, role: user.role,
      ip, loginAt: user.lastLogin!, lastSeen: user.lastLogin!, ua,
    });

    await this.audit(user.id, user.login, 'LOGIN_OK', `Role:${user.role} IP:${ip}`, ip);
    this.logger.log(`Вход: ${user.login} (${user.role}) с ${ip}`);

    return {
      token,
      user: {
        id:     user.id,
        login:  user.login,
        name:   user.name,
        role:   user.role,
        dept:   user.dept,
        ext:    user.ext,
        sipExt: user.sipExt,
      },
    };
  }

  async logout(jti: string, userId: string, login: string, ip: string): Promise<void> {
    this.sessions.delete(jti);
    await this.audit(userId, login, 'LOGOUT', '', ip);
    this.logger.log(`Выход: ${login}`);
  }

  /** Обновить lastSeen для сессии (вызывается из JwtStrategy.validate) */
  touchSession(jti: string): void {
    const s = this.sessions.get(jti);
    if (s) s.lastSeen = new Date().toISOString();
  }

  getMe(userId: string) {
    const users = this.readUsers();
    const u     = users.find(x => x.id === userId);
    if (!u) throw new NotFoundException('Пользователь не найден');
    const { passHash: _, ...pub } = u as any;
    return pub;
  }

  getOnline(): ActiveSession[] {
    return Array.from(this.sessions.values());
  }

  async forgotPassword(email: string, baseUrl: string, ip: string) {
    const users = this.readUsers();
    const user  = users.find(u => u.email?.toLowerCase() === email.toLowerCase());
    if (!user) return { ok: true, emailSent: false };

    const token = crypto.randomBytes(32).toString('hex');
    const resets = this.readResets();
    resets.push({
      token,
      userId:    user.id,
      login:     user.login,
      email,
      createdAt: new Date().toISOString(),
      expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
      used:      false,
    });
    await this.writeResets(resets);

    const link = `${baseUrl}/?reset=${token}`;
    const sent = await this.sendEmail(
      email,
      'Сброс пароля ИНФУД',
      `<p>Ссылка действительна 15 минут:<br><a href="${link}">${link}</a></p>`
    );
    await this.audit(user.id, user.login, 'PW_RESET_REQ', `email:${email}`, ip);

    const result: any = { ok: true, emailSent: sent };
    if (process.env.NODE_ENV === 'development') result.devToken = token;
    return result;
  }

  async resetPassword(token: string, password: string, ip: string): Promise<void> {
    const resets = this.readResets();
    const r      = resets.find(x => x.token === token && !x.used);
    if (!r) throw new BadRequestException('Токен недействителен');
    if (new Date() > new Date(r.expiresAt)) throw new BadRequestException('Токен истёк (15 мин)');
    if (password.length < 6) throw new BadRequestException('Пароль слишком короткий');

    const users = this.readUsers();
    const u     = users.find(x => x.id === r.userId);
    if (!u) throw new NotFoundException('Пользователь не найден');

    u.passHash = await bcrypt.hash(password, SALT_ROUNDS);
    await this.writeUsers(users);
    r.used = true;
    await this.writeResets(resets);
    await this.audit(u.id, u.login, 'PW_RESET', 'via email token', ip);
  }

  get sessionCount(): number { return this.sessions.size; }

  // ─────────────────────────────────────────────────────────────
  // IP SECURITY
  // ─────────────────────────────────────────────────────────────

  scoreIP(ip: string, pts: number): void {
    const now = Date.now();
    let sc    = this.ipScores.get(ip);
    if (!sc || now - sc.ts > 3_600_000) sc = { score: 0, ts: now };
    sc.score += pts;
    this.ipScores.set(ip, sc);
    if (sc.score >= 100) {
      this.ipBans.set(ip, now + 3_600_000);
      this.logger.warn(`IP заблокирован: ${ip} (score=${sc.score})`);
    }
  }

  isBanned(ip: string): boolean {
    const until = this.ipBans.get(ip);
    if (!until) return false;
    if (Date.now() > until) { this.ipBans.delete(ip); return false; }
    return true;
  }

  unban(ip: string): void { this.ipBans.delete(ip); this.ipScores.delete(ip); }
  getBannedIPs() { return Array.from(this.ipBans.entries()).map(([ip, ts]) => ({ ip, until: new Date(ts).toISOString() })); }

  // ─────────────────────────────────────────────────────────────
  // PRIVATE: JSON persistence
  // ─────────────────────────────────────────────────────────────

  private get dataDir(): string {
    return process.env.DATA_DIR ?? path.join(process.cwd(), 'data');
  }

  private readJson<T>(file: string): T[] {
    const p = path.join(this.dataDir, file + '.json');
    try { return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : []; }
    catch (_) { return []; }
  }

  private async writeJson(file: string, data: unknown[]): Promise<void> {
    fs.mkdirSync(this.dataDir, { recursive: true });
    const p   = path.join(this.dataDir, file + '.json');
    const tmp = p + '.tmp';
    fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
    fs.renameSync(tmp, p);
  }

  private readUsers():  UserRecord[]          { return this.readJson<UserRecord>('users'); }
  private writeUsers(d: UserRecord[])         { return this.writeJson('users', d); }
  private readResets(): any[]                 { return this.readJson<any>('pwreset'); }
  private writeResets(d: any[])               { return this.writeJson('pwreset', d); }

  private async audit(
    userId: string | null, login: string,
    action: string, detail: string, ip: string
  ): Promise<void> {
    const a = this.readJson<any>('audit');
    a.push({ ts: new Date().toISOString(), userId, login, action, detail, ip, cat: 'auth' });
    if (a.length > 90 * 2000) a.splice(0, a.length - 90 * 2000);
    await this.writeJson('audit', a);
  }

  private async sendEmail(to: string, subject: string, html: string): Promise<boolean> {
    const cfg = this.appCfg.getPublic() as any;
    if (!cfg.smtpEnabled || !cfg.smtpHost) return false;
    try {
      const nodemailer = require('nodemailer');
      const t = nodemailer.createTransport({
        host: cfg.smtpHost, port: cfg.smtpPort ?? 587,
        secure: cfg.smtpPort === 465,
        auth: { user: cfg.smtpUser, pass: cfg.smtpPass },
      });
      await t.sendMail({ from: cfg.smtpFrom ?? 'noreply@infud.local', to, subject, html });
      return true;
    } catch (e) {
      this.logger.warn(`Email ошибка: ${(e as Error).message}`);
      return false;
    }
  }

  private cleanupSessions(): void {
    const threshold = 9 * 3_600_000;
    for (const [jti, s] of this.sessions) {
      if (Date.now() - new Date(s.lastSeen).getTime() > threshold) {
        this.sessions.delete(jti);
      }
    }
  }

  private async ensureDefaultUsers(): Promise<void> {
    const users = this.readUsers();
    if (users.length > 0) return;

    this.logger.log('Создаём пользователей по умолчанию...');
    const defaults = [
      { login:'admin',     pass:'admin123', name:'Орлов Григорий', role:'admin',      dept:'Администрация',ext:'вн.100',email:'admin@infud.local',sipExt:'100'},
      { login:'tustov',    pass:'shift2026',name:'Тустов А.В.',    role:'dispatcher', dept:'Смена II',     ext:'вн.231',email:'',sipExt:'231'},
      { login:'shchipilo', pass:'tech2026', name:'Щипило И.П.',    role:'tech',       dept:'Технология',   ext:'вн.245',email:'',sipExt:'245'},
      { login:'klimov',    pass:'eng2026',  name:'Климов С.А.',    role:'engineer',   dept:'Механика',     ext:'вн.502',email:'',sipExt:'502'},
      { login:'smelkova',  pass:'disp2026', name:'Смелкова Е.С.',  role:'dispatcher', dept:'СЦ',           ext:'вн.312',email:'',sipExt:'312'},
    ];
    const list: UserRecord[] = [];
    for (const d of defaults) {
      list.push({
        id: 'u_' + crypto.randomBytes(8).toString('hex'),
        login: d.login, passHash: await bcrypt.hash(d.pass, SALT_ROUNDS),
        name: d.name, role: d.role, dept: d.dept, ext: d.ext,
        email: d.email, sipExt: d.sipExt,
        active: true, loginAttempts: 0, lockedUntil: null,
        twoFactorEnabled: false,
        createdAt: new Date().toISOString(), lastLogin: null, lastIp: null,
      });
    }
    await this.writeUsers(list);
    this.logger.log(`Пользователи созданы: ${list.map(u => u.login).join(', ')}`);
  }
}
