// src/clients/client-signed-doc.service.ts
import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { createReadStream, existsSync, statSync } from 'fs';
import { join, normalize, basename, extname } from 'path';
import { Readable } from 'stream';

import { ClientsService } from './clients.service';
import type { ClientSource } from './clients.service';

/**
 * ════════════════════════════════════════════════════════════════════
 * Signed client documents — same two strategies as AuditReportFileService:
 *
 *   A) getPublicUrl → { url, filename, docname }
 *      The frontend just does window.open(url).
 *   B) openFile     → stream the bytes from disk through Nest.
 *
 * Data lives on clients__clientdatas only:
 *   signed_docs     — the stored relative path (like stg1/stg2_audit_report)
 *   signeddocsname  — the human label entered in the legacy CRM
 * Surveillance rows (newsurve__surves) have neither column.
 *
 * Unlike the audit-report version, this one is OWNER-SCOPED — the same
 * rule getClientDetails applies: 403 if the row belongs to another user
 * and the caller can't view all.
 * ════════════════════════════════════════════════════════════════════
 */
@Injectable()
export class ClientSignedDocService {
  private readonly logger = new Logger(ClientSignedDocService.name);

  // Same env vars the audit-report file service reads, so file-storage
  // config stays in one place per environment.
  private readonly PUBLIC_BASE: Record<ClientSource, string> = {
    QRS: process.env.QRS_FILE_BASE_URL || 'https://crm.qrs.ae/',
    TQS: process.env.TQS_FILE_BASE_URL || 'https://crm.tqs.ae/',
  };
  // Absolute folder the legacy `assets/...` paths are relative to (Strategy B).
  private readonly DISK_ROOT: Record<ClientSource, string> = {
    QRS: process.env.QRS_FILE_ROOT || '/var/www/qrs/public',
    TQS: process.env.TQS_FILE_ROOT || '/var/www/tqs/public',
  };

  constructor(
    @InjectDataSource('qrs') private readonly qrs: DataSource,
    @InjectDataSource('tqs') private readonly tqs: DataSource,
    private readonly clientsService: ClientsService,
  ) {}

  /** Read the stored path + display name for one client, ownership-scoped. */
  private async lookup(
    source: ClientSource,
    recordId: number,
    currentUserId: number,
  ): Promise<{ path: string; docname: string | null }> {
    const ds = source === 'QRS' ? this.qrs : this.tqs;
    const rows = await ds.query(
      `SELECT signed_docs AS path, signeddocsname AS docname, user_id
         FROM clients__clientdatas
        WHERE id = ?
        LIMIT 1`,
      [recordId],
    );
    const row = rows?.[0];
    if (!row) {
      throw new NotFoundException(`Client ${recordId} not found in ${source}`);
    }

    // 🔒 Same ownership rule as getClientDetails.
    const canViewAll = await this.clientsService.userCanViewAll(currentUserId);
    if (!canViewAll) {
      const sourceUid = await this.clientsService.resolveSourceUserId(
        currentUserId,
        source,
      );
      if (sourceUid == null || Number(row.user_id) !== sourceUid) {
        throw new ForbiddenException('This client belongs to another user.');
      }
    }

    const path = String(row.path || '').trim();
    if (!path) {
      throw new NotFoundException(
        `No signed document uploaded for ${source} client #${recordId}`,
      );
    }
    const docname = String(row.docname || '').trim() || null;
    return { path, docname };
  }

  /**
   * Display name: prefer the stored signeddocsname; make sure it carries
   * the real file extension so downloads/openers behave.
   */
  private displayName(path: string, docname: string | null): string {
    const real = basename(path);
    if (!docname) return real;
    const ext = extname(real);
    return ext && !docname.toLowerCase().endsWith(ext.toLowerCase())
      ? `${docname}${ext}`
      : docname;
  }

  /**
   * STRATEGY A — return a public URL the browser can open directly.
   * The frontend just does window.open(url).
   */
  async getPublicUrl(
    source: ClientSource,
    recordId: number,
    currentUserId: number,
  ): Promise<{ url: string; filename: string; docname: string }> {
    const { path, docname } = await this.lookup(source, recordId, currentUserId);
    const base = this.PUBLIC_BASE[source].replace(/\/+$/, '');
    const clean = path.replace(/^\/+/, '');
    return {
      url: `${base}/${clean}`,
      filename: basename(clean),
      docname: this.displayName(clean, docname),
    };
  }

  /**
   * STRATEGY B — stream the file bytes from disk.
   * Returns a stream the controller wraps in a StreamableFile.
   */
  async openFile(
    source: ClientSource,
    recordId: number,
    currentUserId: number,
  ): Promise<{ stream: Readable; size: number; mimeType: string; filename: string }> {
    const { path: rel, docname } = await this.lookup(
      source,
      recordId,
      currentUserId,
    );

    // Resolve safely under the configured root (prevent path traversal).
    const root = this.DISK_ROOT[source];
    const abs = normalize(join(root, rel.replace(/^\/+/, '')));
    if (!abs.startsWith(normalize(root))) {
      throw new BadRequestException('Resolved path escapes storage root');
    }
    if (!existsSync(abs)) {
      this.logger.warn(`[CLIENT-SIGNED-DOC] missing on disk: ${abs}`);
      throw new NotFoundException(`File not found on server: ${basename(rel)}`);
    }

    const size = statSync(abs).size;
    return {
      stream: createReadStream(abs),
      size,
      mimeType: this.mimeFor(extname(abs)),
      // Serve it under the signed-doc name, same name the table shows.
      filename: this.displayName(abs, docname),
    };
  }

  private mimeFor(ext: string): string {
    const e = ext.toLowerCase();
    if (e === '.pdf') return 'application/pdf';
    if (e === '.doc') return 'application/msword';
    if (e === '.docx')
      return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
    if (e === '.xls') return 'application/vnd.ms-excel';
    if (e === '.xlsx')
      return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
    if (e === '.png') return 'image/png';
    if (e === '.jpg' || e === '.jpeg') return 'image/jpeg';
    return 'application/octet-stream';
  }
}
