import { FileSystem, BaseDir, AppPlatform, ResolvedPath, FileItem } from '@/types/system';
import { DatabaseOpts, DatabaseService } from '@/types/database';
import { SchemaType } from '@/services/database/migrate';
import { getOSPlatform, isValidURL } from '@/utils/misc';
import { isSafariBrowser } from '@/utils/ua';
import { RemoteFile } from '@/utils/file';
import { detectViewTransitionGroup, detectViewTransitionsAPI } from '@/utils/viewTransition';
import { isPWA } from './environment';
import { BaseAppService } from './appService';
import {
  DATA_SUBDIR,
  LOCAL_BOOKS_SUBDIR,
  LOCAL_DICTIONARIES_SUBDIR,
  LOCAL_FONTS_SUBDIR,
  LOCAL_IMAGES_SUBDIR,
} from './constants';

const basePrefix = async () => '';

const resolvePath = (path: string, base: BaseDir): ResolvedPath => {
  switch (base) {
    case 'Data':
      return { baseDir: 0, basePrefix, fp: `${DATA_SUBDIR}/${path}`, base };
    case 'Books':
      return { baseDir: 0, basePrefix, fp: `${LOCAL_BOOKS_SUBDIR}/${path}`, base };
    case 'Fonts':
      return { baseDir: 0, basePrefix, fp: `${LOCAL_FONTS_SUBDIR}/${path}`, base };
    case 'Images':
      return { baseDir: 0, basePrefix, fp: `${LOCAL_IMAGES_SUBDIR}/${path}`, base };
    case 'Dictionaries':
      return { baseDir: 0, basePrefix, fp: `${LOCAL_DICTIONARIES_SUBDIR}/${path}`, base };
    case 'None':
      return { baseDir: 0, basePrefix, fp: path, base };
    default:
      return { baseDir: 0, basePrefix, fp: `${base}/${path}`, base };
  }
};

const dbName = 'AppFileSystem';
const dbVersion = 1;

async function openIndexedDB(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(dbName, dbVersion);

    request.onupgradeneeded = () => {
      const db = request.result;
      if (!db.objectStoreNames.contains('files')) {
        db.createObjectStore('files', { keyPath: 'path' });
      }
    };

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

const indexedDBFileSystem: FileSystem = {
  resolvePath,
  async getPrefix(base: BaseDir) {
    const { basePrefix, fp } = this.resolvePath('', base);
    const basePath = await basePrefix();
    const prefix = fp ? (basePath ? `${basePath}/${fp}` : fp) : basePath;
    return prefix.replace(/\/+$/, '');
  },
  getURL(path: string) {
    if (isValidURL(path)) {
      return path;
    } else {
      return URL.createObjectURL(new Blob([path]));
    }
  },
  async getBlobURL(path: string, base: BaseDir) {
    try {
      const content = await this.readFile(path, base, 'binary');
      return URL.createObjectURL(new Blob([content]));
    } catch {
      return path;
    }
  },
  async getImageURL(path: string) {
    return await this.getBlobURL(path, 'None');
  },
  async openFile(path: string, base: BaseDir, filename?: string) {
    if (isValidURL(path)) {
      return await new RemoteFile(path, filename).open();
    } else {
      const content = await this.readFile(path, base, 'binary');
      return new File([content], filename || path);
    }
  },
  async copyFile(srcPath: string, srcBase: BaseDir, dstPath: string, dstBase: BaseDir) {
    const { fp: srcFp } = this.resolvePath(srcPath, srcBase);
    const { fp: dstFp } = this.resolvePath(dstPath, dstBase);
    const db = await openIndexedDB();

    return new Promise<void>((resolve, reject) => {
      const transaction = db.transaction('files', 'readwrite');
      const store = transaction.objectStore('files');
      const getRequest = store.get(srcFp);

      getRequest.onsuccess = () => {
        const data = getRequest.result;
        if (data) {
          store.put({ path: dstFp, content: data.content });
          resolve();
        } else {
          reject(new Error(`File not found: ${srcFp}`));
        }
      };

      getRequest.onerror = () => reject(getRequest.error);
    });
  },
  async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') {
    const { fp } = this.resolvePath(path, base);
    const db = await openIndexedDB();

    return new Promise<string | ArrayBuffer>((resolve, reject) => {
      const transaction = db.transaction('files', 'readonly');
      const store = transaction.objectStore('files');
      const request = store.get(fp);

      request.onsuccess = async () => {
        if (request.result) {
          const content = request.result.content;
          if (mode === 'text') resolve(content);
          else {
            if (content instanceof Blob) {
              const arrayBuffer = await content.arrayBuffer();
              resolve(arrayBuffer);
            } else if (content instanceof ArrayBuffer) {
              resolve(content);
            } else if (typeof content === 'string') {
              resolve(new TextEncoder().encode(content).buffer as ArrayBuffer);
            } else {
              reject(new Error('Unsupported content type in IndexedDB'));
            }
          }
        } else {
          reject(new Error(`File not found: ${fp}`));
        }
      };

      request.onerror = () => reject(request.error);
    });
  },
  async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File) {
    const { fp } = this.resolvePath(path, base);
    const db = await openIndexedDB();

    if (content instanceof File) {
      content = await content.arrayBuffer();
    }
    return new Promise<void>((resolve, reject) => {
      const transaction = db.transaction('files', 'readwrite');
      const store = transaction.objectStore('files');

      store.put({ path: fp, content });

      transaction.oncomplete = () => resolve();
      transaction.onerror = () => reject(transaction.error);
    });
  },
  async removeFile(path: string, base: BaseDir) {
    const { fp } = this.resolvePath(path, base);
    const db = await openIndexedDB();

    return new Promise<void>((resolve, reject) => {
      const transaction = db.transaction('files', 'readwrite');
      const store = transaction.objectStore('files');

      store.delete(fp);

      transaction.oncomplete = () => resolve();
      transaction.onerror = () => reject(transaction.error);
    });
  },
  async createDir(path: string, base: BaseDir) {
    return await this.writeFile(path, base, '');
  },
  async removeDir(path: string, base: BaseDir) {
    const { fp } = this.resolvePath(path, base);
    const db = await openIndexedDB();

    return new Promise<void>((resolve, reject) => {
      const transaction = db.transaction('files', 'readwrite');
      const store = transaction.objectStore('files');
      const request = store.getAll();

      request.onsuccess = () => {
        const files = request.result as { path: string }[];
        files.forEach((file) => {
          if (file.path.startsWith(fp)) {
            store.delete(file.path);
          }
        });
      };

      transaction.oncomplete = () => resolve();
      transaction.onerror = () => reject(transaction.error);
    });
  },
  async readDir(path: string, base: BaseDir) {
    const { fp } = this.resolvePath(path, base);
    const prefix = fp.endsWith('/') ? fp : `${fp}/`;
    const db = await openIndexedDB();

    return new Promise<FileItem[]>((resolve, reject) => {
      const transaction = db.transaction('files', 'readonly');
      const store = transaction.objectStore('files');
      const request = store.getAll();

      request.onsuccess = () => {
        const files = request.result as { path: string; content: string | ArrayBuffer | Blob }[];
        resolve(
          files
            .filter((file) => file.path.startsWith(prefix))
            .map((file) => ({
              path: file.path.slice(prefix.length),
              size:
                file.content instanceof Blob
                  ? file.content.size
                  : typeof file.content === 'string'
                    ? file.content.length
                    : file.content instanceof ArrayBuffer
                      ? file.content.byteLength
                      : 0,
            })),
        );
      };

      request.onerror = () => reject(request.error);
    });
  },
  async exists(path: string, base: BaseDir) {
    const { fp } = this.resolvePath(path, base);
    const db = await openIndexedDB();

    return new Promise<boolean>((resolve, reject) => {
      const transaction = db.transaction('files', 'readonly');
      const store = transaction.objectStore('files');
      const request = store.get(fp);

      request.onsuccess = () => resolve(!!request.result);
      request.onerror = () => reject(request.error);
    });
  },
  async stats(path: string, base: BaseDir) {
    const { fp } = this.resolvePath(path, base);
    const db = await openIndexedDB();

    return new Promise((resolve, reject) => {
      const transaction = db.transaction('files', 'readonly');
      const store = transaction.objectStore('files');
      const request = store.get(fp);

      request.onsuccess = () => {
        const result = request.result;
        if (result) {
          const content = result.content;
          const size =
            content instanceof Blob
              ? content.size
              : typeof content === 'string'
                ? content.length
                : content instanceof ArrayBuffer
                  ? content.byteLength
                  : 0;
          resolve({
            isFile: true,
            isDirectory: false,
            size,
            mtime: null,
            atime: null,
            birthtime: null,
          });
        } else {
          reject(new Error(`File not found: ${fp}`));
        }
      };

      request.onerror = () => reject(request.error);
    });
  },
};

export class WebAppService extends BaseAppService {
  fs = indexedDBFileSystem;
  override isMobile = ['android', 'ios'].includes(getOSPlatform());
  override appPlatform = 'web' as AppPlatform;
  override supportsCanvasContext2DFilter = !isSafariBrowser();
  override supportsViewTransitionsAPI = detectViewTransitionsAPI();
  override supportsViewTransitionGroup = detectViewTransitionGroup();
  override hasSafeAreaInset = isPWA();

  override async init() {
    await this.loadSettings();
    await this.prepareBooksDir();
    await this.runMigrations();
  }

  override async runMigrations() {
    try {
      const settings = await this.loadSettings();
      const lastMigrationVersion = settings.migrationVersion || 0;

      await super.runMigrations(lastMigrationVersion, settings);

      if (lastMigrationVersion < this.CURRENT_MIGRATION_VERSION) {
        await this.saveSettings({
          ...settings,
          migrationVersion: this.CURRENT_MIGRATION_VERSION,
        });
      }
    } catch (error) {
      console.error('Failed to run migrations:', error);
    }
  }

  override resolvePath(fp: string, base: BaseDir): ResolvedPath {
    return this.fs.resolvePath(fp, base);
  }

  async setCustomRootDir() {
    // No-op in web environment
  }

  async selectDirectory(): Promise<string> {
    throw new Error('selectDirectory is not supported in browser');
  }

  async selectFiles(): Promise<string[]> {
    throw new Error('selectFiles is not supported in browser');
  }

  async saveFile(
    filename: string,
    content: string | ArrayBuffer | null,
    options?: {
      filePath?: string;
      mimeType?: string;
      share?: boolean;
      // Web ignores `sharePosition` — `navigator.share()` anchors itself
      // natively to the calling element on Safari / iOS.
      sharePosition?: { x: number; y: number; preferredEdge?: 'top' | 'bottom' | 'left' | 'right' };
    },
  ): Promise<boolean> {
    const mimeType = options?.mimeType || 'application/octet-stream';
    // Web has no filesystem path to read from, so `null` content (only the
    // native-only "Send" flow passes it) degrades to an empty body.
    const body = content ?? '';
    if (
      options?.share &&
      typeof navigator !== 'undefined' &&
      typeof navigator.share === 'function'
    ) {
      let shareData: ShareData | null = null;
      try {
        const file = new File([body], filename, { type: mimeType });
        const candidate: ShareData = { files: [file], title: filename };
        if (typeof navigator.canShare !== 'function' || navigator.canShare(candidate)) {
          shareData = candidate;
        }
      } catch (error) {
        // File constructor unavailable or rejected the input; fall through to download.
        console.warn('Failed to build share file; falling back to download:', error);
      }
      if (shareData) {
        try {
          await navigator.share(shareData);
          return true;
        } catch (error) {
          // AbortError = user dismissed the sheet; respect that as an explicit
          // "don't share" choice. Any other error (e.g., NotAllowedError on
          // desktop Chrome where canShare lies about file support) means the
          // share never happened — fall through to the download fallback.
          if ((error as DOMException)?.name === 'AbortError') {
            return true;
          }
          console.warn('navigator.share failed; falling back to download:', error);
        }
      }
    }
    try {
      const blob = new Blob([body], { type: mimeType });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      return true;
    } catch (error) {
      console.error('Failed to save file:', error);
      return false;
    }
  }

  // No system photo gallery on the web; callers fall back to the saveFile flow.
  async saveImageToGallery(): Promise<boolean> {
    return false;
  }

  async ask(message: string): Promise<boolean> {
    return window.confirm(message);
  }

  async openDatabase(
    schema: SchemaType,
    path: string,
    base: BaseDir,
    opts?: DatabaseOpts,
  ): Promise<DatabaseService> {
    const fullPath = await this.resolveFilePath(path, base);
    // OPFS `getFileHandle` rejects names containing path separators, and the
    // Turso WASM connector passes the whole string as a single OPFS handle
    // name without traversing directories. Flatten to a safe single segment.
    const opfsName = fullPath.replace(/[/\\]+/g, '_').replace(/^_+/, '');
    const { WebDatabaseService } = await import('./database/webDatabaseService');
    const db = await WebDatabaseService.open(opfsName, opts);
    const { migrate } = await import('./database/migrate');
    const { getMigrations } = await import('./database/migrations');
    await migrate(db, getMigrations(schema));
    return db;
  }
}
