import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { promises as fsp, existsSync } from 'node:fs';
import { dirname, resolve, sep } from 'node:path';
import { randomBytes } from 'node:crypto';
import { resolveStorageRoot, STORAGE_PUBLIC_MOUNT } from '../../config/storage.config';
import { isAllowedUploadExtension } from './file-sniff';
import { StorageService, type StoredObject } from './storage.service';
import { DELETED_MEDIA_ROOT, isMediaFolder, type MediaFolder } from '../media/media.constants';

const SAFE_SEGMENT = /^(?!^\.\.?$)[a-zA-Z0-9._-]+$/;
const FOLDER_SEGMENT = /^(used|unused)$/;

/**
 * Local-disk implementation of {@link StorageService}.
 *
 * Objects live under `STORAGE_LOCAL_DIR` (default `./uploads`) and are
 * served by the Express static mount at {@link STORAGE_PUBLIC_MOUNT}.
 * All path-safety (traversal, tenant-prefix shape) lives here so
 * callers can pass keys derived from user input without re-checking.
 */
@Injectable()
export class LocalStorageService extends StorageService {
  private readonly root: string;
  private readonly publicBase = STORAGE_PUBLIC_MOUNT;

  constructor(config: ConfigService) {
    super();
    this.root = resolveStorageRoot(config.get<string>('storage.localDir'));
  }

  async save(
    prefix: string,
    extension: string,
    body: Buffer,
    folder?: MediaFolder,
  ): Promise<StoredObject> {
    const ext = (extension || '').toLowerCase();
    if (!isAllowedUploadExtension(ext)) {
      throw new Error('Unsupported file extension');
    }
    const filename = `${Date.now()}-${randomBytes(8).toString('hex')}${ext}`;
    const base = this.safeSegment(prefix) || 'default';
    const key = folder ? `${base}/${folder}/${filename}` : `${base}/${filename}`;
    const abs = this.absPath(key);
    await fsp.mkdir(dirname(abs), { recursive: true });
    await fsp.writeFile(abs, body);
    return { key, url: this.url(key) };
  }

  async move(fromKey: string, toKey: string): Promise<void> {
    const from = this.absPath(fromKey);
    const to = this.absPath(toKey);
    if (!existsSync(from)) return;
    await fsp.mkdir(dirname(to), { recursive: true });
    await fsp.rename(from, to);
  }

  async delete(key: string): Promise<void> {
    await fsp.rm(this.absPath(key), { force: true });
  }

  async exists(key: string): Promise<boolean> {
    return existsSync(this.absPath(key));
  }

  url(key: string): string {
    return `${this.publicBase}/${this.assertSafeKey(key)}`;
  }

  keyFromUrl(url: string): string | null {
    if (!url) return null;
    const pathname = this.pathnameOf(url);
    if (!pathname) return null;
    if (pathname !== this.publicBase && !pathname.startsWith(`${this.publicBase}/`)) {
      return null;
    }
    const key = pathname.slice(this.publicBase.length + 1);
    if (!key || !this.isSafeKey(key)) return null;
    return key;
  }

  // ── internals ──────────────────────────────────────────────────────

  private safeSegment(s: string): string {
    return (s || '').replace(/[^a-zA-Z0-9._-]/g, '');
  }

  /**
   * Allowed keys:
   *   {subdomain}/{filename}
   *   {subdomain}/used/{filename}
   *   {subdomain}/unused/{filename}
   *   {subdomain}/{mediaFolder}/{filename}
   *   deleted/{subdomain}/{mediaFolder}/{filename}
   * Each segment is `[A-Za-z0-9._-]`, never `.` / `..`, never `\` / NUL.
   */
  private isSafeKey(key: string): boolean {
    if (!key || key.includes('\\') || key.includes('\0') || key.includes('..')) {
      return false;
    }
    const parts = key.split('/');
    if (!parts.every((p) => SAFE_SEGMENT.test(p))) return false;
    if (parts.length === 2) return true;
    if (parts.length === 3) {
      return FOLDER_SEGMENT.test(parts[1]) || isMediaFolder(parts[1]);
    }
    if (parts.length === 4) {
      return parts[0] === DELETED_MEDIA_ROOT && isMediaFolder(parts[2]);
    }
    return false;
  }

  private assertSafeKey(key: string): string {
    if (!this.isSafeKey(key)) {
      throw new Error('Unsafe storage key');
    }
    return key;
  }

  /**
   * Resolve a key to an absolute path AND assert it stays inside the
   * storage root. Shape is validated first so `unused/../../other-tenant`
   * never reaches `resolve()`.
   */
  private absPath(key: string): string {
    this.assertSafeKey(key);
    const abs = resolve(this.root, key);
    if (abs !== this.root && !abs.startsWith(this.root + sep)) {
      throw new Error('Unsafe storage key — path escapes storage root');
    }
    return abs;
  }

  /** Pathname only — query/hash stripped. Host is ignored; tenant bind happens at the caller. */
  private pathnameOf(raw: string): string | null {
    const trimmed = raw.trim();
    if (!trimmed) return null;
    try {
      if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed) || trimmed.startsWith('//')) {
        const parsed = new URL(trimmed, 'https://invalid.invalid');
        return decodeURIComponent(parsed.pathname);
      }
      return decodeURIComponent(trimmed.split('?')[0].split('#')[0]);
    } catch {
      return null;
    }
  }
}
