import { Injectable } from '@angular/core';
import { MediaRepository } from './media-repository';
import { MediaAsset, MediaAssetKind, MediaListParams, MediaListResult, MediaUploadOptions } from './models/media-asset.model';

const DB_NAME = 'media-manager';
const DB_VERSION = 1;
const STORE_NAME = 'assets';
const DEFAULT_PAGE_SIZE = 24;
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
const MAX_IMAGE_DIMENSION = 2000;
const COMPRESS_QUALITY = 0.85;
const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'application/pdf'];

interface StoredAssetRecord {
  id: string;
  filename: string;
  mimeType: string;
  size: number;
  width?: number;
  height?: number;
  altText?: Record<string, string>;
  caption?: string;
  description?: string;
  decorative?: boolean;
  tags?: string[];
  folder?: string;
  createdAt: string;
  blob: Blob;
}

function assetKind(mimeType: string): MediaAssetKind {
  if (mimeType === 'image/svg+xml') return 'svg';
  if (mimeType === 'application/pdf') return 'pdf';
  if (mimeType.startsWith('image/')) return 'image';
  return 'other';
}

@Injectable({ providedIn: 'root' })
export class MockMediaRepository extends MediaRepository {
  private db: Promise<IDBDatabase> | null = null;
  private readonly objectUrls = new Map<string, string>();

  async list(params: MediaListParams = {}): Promise<MediaListResult> {
    const page = params.page ?? 1;
    const pageSize = params.pageSize ?? DEFAULT_PAGE_SIZE;
    const search = (params.search ?? '').trim().toLowerCase();

    const records = await this.getAllRecords();
    const filtered = records
      .filter(record => !search || record.filename.toLowerCase().includes(search))
      .filter(record => !params.folder || record.folder === params.folder)
      .filter(record => !params.tag || (record.tags ?? []).includes(params.tag!))
      .filter(record => !params.kind || assetKind(record.mimeType) === params.kind);

    const sort = params.sort ?? 'recent';
    if (sort === 'name') {
      filtered.sort((a, b) => a.filename.localeCompare(b.filename));
    } else if (sort === 'size') {
      filtered.sort((a, b) => b.size - a.size);
    } else {
      filtered.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
    }

    const start = (page - 1) * pageSize;
    const pageItems = filtered.slice(start, start + pageSize);

    return {
      items: pageItems.map(record => this.toAsset(record)),
      total: filtered.length,
    };
  }

  async upload(file: File, options: MediaUploadOptions = {}): Promise<MediaAsset> {
    this.validateFile(file);
    const blob = await this.prepareBlob(file);
    const dimensions = await this.readImageDimensions(blob, file.type);
    const record: StoredAssetRecord = {
      id: crypto.randomUUID(),
      filename: file.name,
      mimeType: file.type,
      size: blob.size,
      width: dimensions?.width,
      height: dimensions?.height,
      folder: options.folder || undefined,
      tags: options.tags,
      createdAt: new Date().toISOString(),
      blob,
    };

    const db = await this.openDb();
    await this.runTransaction(db, 'readwrite', store => store.add(record));

    return this.toAsset(record);
  }

  async listFolders(): Promise<string[]> {
    const records = await this.getAllRecords();
    return [...new Set(records.map(record => record.folder).filter((folder): folder is string => !!folder))].sort();
  }

  private validateFile(file: File): void {
    if (file.size > MAX_FILE_SIZE_BYTES) {
      throw new Error(`File exceeds the ${MAX_FILE_SIZE_BYTES / (1024 * 1024)}MB limit.`);
    }
    if (!ALLOWED_MIME_TYPES.includes(file.type)) {
      throw new Error(`Unsupported file type: ${file.type || 'unknown'}.`);
    }
  }

  private async prepareBlob(file: File): Promise<Blob> {
    if (file.type === 'image/svg+xml') {
      return this.sanitizeSvg(file);
    }
    if (file.type === 'image/gif') {
      return file;
    }
    if (file.type.startsWith('image/')) {
      return this.compressImage(file);
    }
    return file;
  }

  /** Strips <script> elements and on*="..." event-handler attributes before storing untrusted SVG markup. */
  private async sanitizeSvg(file: File): Promise<Blob> {
    const text = await file.text();
    const sanitized = text
      .replace(/<script[\s\S]*?<\/script>/gi, '')
      .replace(/\son\w+\s*=\s*"[^"]*"/gi, '')
      .replace(/\son\w+\s*=\s*'[^']*'/gi, '');
    return new Blob([sanitized], { type: 'image/svg+xml' });
  }

  private async compressImage(file: File): Promise<Blob> {
    try {
      const bitmap = await createImageBitmap(file);
      const scale = Math.min(1, MAX_IMAGE_DIMENSION / Math.max(bitmap.width, bitmap.height));
      const width = Math.round(bitmap.width * scale);
      const height = Math.round(bitmap.height * scale);
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d');
      if (!ctx) {
        bitmap.close();
        return file;
      }
      ctx.drawImage(bitmap, 0, 0, width, height);
      bitmap.close();
      const outputType = file.type === 'image/png' ? 'image/png' : 'image/jpeg';
      const blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, outputType, COMPRESS_QUALITY));
      return blob ?? file;
    } catch {
      return file;
    }
  }

  async remove(id: string): Promise<void> {
    const db = await this.openDb();
    await this.runTransaction(db, 'readwrite', store => store.delete(id));
    this.revokeObjectUrl(id);
  }

  async update(id: string, patch: Partial<Pick<MediaAsset, 'altText' | 'tags' | 'folder' | 'caption' | 'description' | 'decorative'>>): Promise<MediaAsset> {
    const db = await this.openDb();
    const existing = await this.getRecord(db, id);
    if (!existing) {
      throw new Error(`Media asset not found: ${id}`);
    }

    const updated: StoredAssetRecord = { ...existing, ...patch };
    await this.runTransaction(db, 'readwrite', store => store.put(updated));

    return this.toAsset(updated);
  }

  private toAsset(record: StoredAssetRecord): MediaAsset {
    const url = this.getObjectUrl(record.id, record.blob);
    return {
      id: record.id,
      url,
      thumbnailUrl: url,
      filename: record.filename,
      mimeType: record.mimeType,
      size: record.size,
      width: record.width,
      height: record.height,
      altText: record.altText,
      caption: record.caption,
      description: record.description,
      decorative: record.decorative,
      tags: record.tags,
      folder: record.folder,
      createdAt: record.createdAt,
    };
  }

  private getObjectUrl(id: string, blob: Blob): string {
    const cached = this.objectUrls.get(id);
    if (cached) {
      return cached;
    }
    const url = URL.createObjectURL(blob);
    this.objectUrls.set(id, url);
    return url;
  }

  private revokeObjectUrl(id: string): void {
    const url = this.objectUrls.get(id);
    if (url) {
      URL.revokeObjectURL(url);
      this.objectUrls.delete(id);
    }
  }

  private async readImageDimensions(blob: Blob, mimeType: string): Promise<{ width: number; height: number } | undefined> {
    if (!mimeType.startsWith('image/') || mimeType === 'image/svg+xml') {
      return undefined;
    }
    try {
      const bitmap = await createImageBitmap(blob);
      const dimensions = { width: bitmap.width, height: bitmap.height };
      bitmap.close();
      return dimensions;
    } catch {
      return undefined;
    }
  }

  private openDb(): Promise<IDBDatabase> {
    if (!this.db) {
      this.db = new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);
        request.onupgradeneeded = () => {
          const db = request.result;
          if (!db.objectStoreNames.contains(STORE_NAME)) {
            db.createObjectStore(STORE_NAME, { keyPath: 'id' });
          }
        };
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
    }
    return this.db;
  }

  private getAllRecords(): Promise<StoredAssetRecord[]> {
    return this.openDb().then(db => new Promise((resolve, reject) => {
      const store = db.transaction(STORE_NAME, 'readonly').objectStore(STORE_NAME);
      const request = store.getAll();
      request.onsuccess = () => resolve(request.result as StoredAssetRecord[]);
      request.onerror = () => reject(request.error);
    }));
  }

  private getRecord(db: IDBDatabase, id: string): Promise<StoredAssetRecord | undefined> {
    return new Promise((resolve, reject) => {
      const store = db.transaction(STORE_NAME, 'readonly').objectStore(STORE_NAME);
      const request = store.get(id);
      request.onsuccess = () => resolve(request.result as StoredAssetRecord | undefined);
      request.onerror = () => reject(request.error);
    });
  }

  private runTransaction(
    db: IDBDatabase,
    mode: IDBTransactionMode,
    action: (store: IDBObjectStore) => IDBRequest
  ): Promise<void> {
    return new Promise((resolve, reject) => {
      const transaction = db.transaction(STORE_NAME, mode);
      const store = transaction.objectStore(STORE_NAME);
      action(store);
      transaction.oncomplete = () => resolve();
      transaction.onerror = () => reject(transaction.error);
    });
  }
}
