import * as fs from 'node:fs';
import * as fsp from 'node:fs/promises';
import * as nodePath from 'node:path';
import * as os from 'node:os';
import { pathToFileURL } from 'node:url';

import { FileSystem, BaseDir, OsPlatform, ResolvedPath, FileItem, FileInfo } from '@/types/system';
import { DatabaseOpts, DatabaseService } from '@/types/database';
import { SchemaType } from '@/services/database/migrate';
import { BaseAppService } from './appService';
import {
  DATA_SUBDIR,
  LOCAL_BOOKS_SUBDIR,
  LOCAL_DICTIONARIES_SUBDIR,
  LOCAL_FONTS_SUBDIR,
  LOCAL_IMAGES_SUBDIR,
} from './constants';

const APP_NAME = 'Readest';

// System directory getters matching Tauri's appDataDir, appConfigDir, etc.
function getAppDataDir(): string {
  switch (process.platform) {
    case 'darwin':
      return nodePath.join(os.homedir(), 'Library', 'Application Support', APP_NAME);
    case 'win32':
      return nodePath.join(
        process.env['APPDATA'] || nodePath.join(os.homedir(), 'AppData', 'Roaming'),
        APP_NAME,
      );
    default:
      return nodePath.join(
        process.env['XDG_DATA_HOME'] || nodePath.join(os.homedir(), '.local', 'share'),
        APP_NAME,
      );
  }
}

function getAppConfigDir(): string {
  switch (process.platform) {
    case 'darwin':
      return nodePath.join(os.homedir(), 'Library', 'Application Support', APP_NAME);
    case 'win32':
      return nodePath.join(
        process.env['APPDATA'] || nodePath.join(os.homedir(), 'AppData', 'Roaming'),
        APP_NAME,
      );
    default:
      return nodePath.join(
        process.env['XDG_CONFIG_HOME'] || nodePath.join(os.homedir(), '.config'),
        APP_NAME,
      );
  }
}

function getAppCacheDir(): string {
  switch (process.platform) {
    case 'darwin':
      return nodePath.join(os.homedir(), 'Library', 'Caches', APP_NAME);
    case 'win32':
      return nodePath.join(
        process.env['LOCALAPPDATA'] || nodePath.join(os.homedir(), 'AppData', 'Local'),
        APP_NAME,
        'Cache',
      );
    default:
      return nodePath.join(
        process.env['XDG_CACHE_HOME'] || nodePath.join(os.homedir(), '.cache'),
        APP_NAME,
      );
  }
}

function getAppLogDir(): string {
  switch (process.platform) {
    case 'darwin':
      return nodePath.join(os.homedir(), 'Library', 'Logs', APP_NAME);
    case 'win32':
      return nodePath.join(
        process.env['LOCALAPPDATA'] || nodePath.join(os.homedir(), 'AppData', 'Local'),
        APP_NAME,
        'Logs',
      );
    default:
      return nodePath.join(
        process.env['XDG_STATE_HOME'] || nodePath.join(os.homedir(), '.local', 'state'),
        APP_NAME,
      );
  }
}

function getTempDir(): string {
  return nodePath.join(os.tmpdir(), APP_NAME);
}

// Path resolver matching nativeAppService's getPathResolver pattern.
// When customRootDir is set, Settings/Data/Books/Fonts/Images all resolve under it.
// Otherwise they use standard system directories.
const getPathResolver = ({ customRootDir }: { customRootDir?: string } = {}) => {
  const isCustomBaseDir = Boolean(customRootDir);
  const getCustomBasePrefix = isCustomBaseDir
    ? (base: BaseDir) => {
        const dataDirs: BaseDir[] = [
          'Settings',
          'Data',
          'Books',
          'Fonts',
          'Images',
          'Dictionaries',
        ];
        const leafDir = dataDirs.includes(base) ? '' : base;
        return leafDir ? nodePath.join(customRootDir!, leafDir) : customRootDir!;
      }
    : undefined;

  return (fp: string, base: BaseDir): ResolvedPath => {
    const custom = getCustomBasePrefix?.(base);
    switch (base) {
      case 'Settings':
        return {
          baseDir: 0,
          basePrefix: async () => custom ?? getAppConfigDir(),
          fp: custom ? nodePath.join(custom, fp) : fp,
          base,
        };
      case 'Cache':
        return {
          baseDir: 0,
          basePrefix: async () => getAppCacheDir(),
          fp,
          base,
        };
      case 'Log':
        return {
          baseDir: 0,
          basePrefix: async () => custom ?? getAppLogDir(),
          fp: custom ? nodePath.join(custom, fp) : fp,
          base,
        };
      case 'Data':
        return {
          baseDir: 0,
          basePrefix: async () => custom ?? getAppDataDir(),
          fp: custom ? nodePath.join(custom, DATA_SUBDIR, fp) : nodePath.join(DATA_SUBDIR, fp),
          base,
        };
      case 'Books':
        return {
          baseDir: 0,
          basePrefix: async () => custom ?? getAppDataDir(),
          fp: custom
            ? nodePath.join(custom, LOCAL_BOOKS_SUBDIR, fp)
            : nodePath.join(LOCAL_BOOKS_SUBDIR, fp),
          base,
        };
      case 'Fonts':
        return {
          baseDir: 0,
          basePrefix: async () => custom ?? getAppDataDir(),
          fp: custom
            ? nodePath.join(custom, LOCAL_FONTS_SUBDIR, fp)
            : nodePath.join(LOCAL_FONTS_SUBDIR, fp),
          base,
        };
      case 'Images':
        return {
          baseDir: 0,
          basePrefix: async () => custom ?? getAppDataDir(),
          fp: custom
            ? nodePath.join(custom, LOCAL_IMAGES_SUBDIR, fp)
            : nodePath.join(LOCAL_IMAGES_SUBDIR, fp),
          base,
        };
      case 'Dictionaries':
        return {
          baseDir: 0,
          basePrefix: async () => custom ?? getAppDataDir(),
          fp: custom
            ? nodePath.join(custom, LOCAL_DICTIONARIES_SUBDIR, fp)
            : nodePath.join(LOCAL_DICTIONARIES_SUBDIR, fp),
          base,
        };
      case 'None':
        return {
          baseDir: 0,
          basePrefix: async () => '',
          fp,
          base,
        };
      case 'Temp':
      default:
        return {
          baseDir: 0,
          basePrefix: async () => getTempDir(),
          fp,
          base,
        };
    }
  };
};

// Resolve an fp from resolvePath to an absolute path.
// When customRootDir is set, fp is already absolute; otherwise join with the base prefix.
async function toAbsolute(resolved: ResolvedPath): Promise<string> {
  if (nodePath.isAbsolute(resolved.fp)) return resolved.fp;
  const prefix = (await resolved.basePrefix()).replace(/\/+$/, '');
  return resolved.fp ? nodePath.join(prefix, resolved.fp) : prefix;
}

export const nodeFileSystem: FileSystem = {
  resolvePath: getPathResolver(),

  async getPrefix(base: BaseDir) {
    return toAbsolute(this.resolvePath('', base));
  },

  getURL(filePath: string) {
    return pathToFileURL(filePath).href;
  },

  async getBlobURL(filePath: string, base: BaseDir) {
    const content = await this.readFile(filePath, base, 'binary');
    return `data:application/octet-stream;base64,${Buffer.from(content as ArrayBuffer).toString('base64')}`;
  },

  async getImageURL(filePath: string) {
    return this.getURL(filePath);
  },

  async openFile(filePath: string, base: BaseDir, name?: string): Promise<File> {
    const fullPath = await toAbsolute(this.resolvePath(filePath, base));
    const buffer = await fsp.readFile(fullPath);
    const fileName = name || nodePath.basename(fullPath);
    return new File([buffer], fileName);
  },

  async copyFile(
    srcPath: string,
    srcBase: BaseDir,
    dstPath: string,
    dstBase: BaseDir,
  ): Promise<void> {
    const fullSrc = await toAbsolute(this.resolvePath(srcPath, srcBase));
    const fullDst = await toAbsolute(this.resolvePath(dstPath, dstBase));
    await fsp.mkdir(nodePath.dirname(fullDst), { recursive: true });
    await fsp.copyFile(fullSrc, fullDst);
  },

  async readFile(
    filePath: string,
    base: BaseDir,
    mode: 'text' | 'binary',
  ): Promise<string | ArrayBuffer> {
    const fullPath = await toAbsolute(this.resolvePath(filePath, base));
    if (mode === 'text') {
      return await fsp.readFile(fullPath, 'utf-8');
    }
    const buffer = await fsp.readFile(fullPath);
    return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
  },

  async writeFile(
    filePath: string,
    base: BaseDir,
    content: string | ArrayBuffer | File,
  ): Promise<void> {
    const fullPath = await toAbsolute(this.resolvePath(filePath, base));
    await fsp.mkdir(nodePath.dirname(fullPath), { recursive: true });
    if (typeof content === 'string') {
      await fsp.writeFile(fullPath, content, 'utf-8');
    } else if (content instanceof File) {
      const buffer = Buffer.from(await content.arrayBuffer());
      await fsp.writeFile(fullPath, buffer);
    } else {
      await fsp.writeFile(fullPath, Buffer.from(content));
    }
  },

  async removeFile(filePath: string, base: BaseDir): Promise<void> {
    const fullPath = await toAbsolute(this.resolvePath(filePath, base));
    await fsp.unlink(fullPath);
  },

  async createDir(dirPath: string, base: BaseDir, recursive = false): Promise<void> {
    const fullPath = await toAbsolute(this.resolvePath(dirPath, base));
    await fsp.mkdir(fullPath, { recursive });
  },

  async removeDir(dirPath: string, base: BaseDir, recursive = false): Promise<void> {
    const fullPath = await toAbsolute(this.resolvePath(dirPath, base));
    await fsp.rm(fullPath, { recursive, force: recursive });
  },

  async readDir(dirPath: string, base: BaseDir): Promise<FileItem[]> {
    const fullPath = await toAbsolute(this.resolvePath(dirPath, base));
    const items: FileItem[] = [];

    const walk = async (dir: string, relative: string) => {
      let entries: fs.Dirent[];
      try {
        entries = await fsp.readdir(dir, { withFileTypes: true });
      } catch {
        return;
      }
      for (const entry of entries) {
        const entryRelative = relative ? nodePath.join(relative, entry.name) : entry.name;
        const entryFull = nodePath.join(dir, entry.name);
        if (entry.isDirectory()) {
          await walk(entryFull, entryRelative);
        } else if (entry.isFile()) {
          const stat = await fsp.stat(entryFull).catch(() => null);
          items.push({ path: entryRelative, size: stat?.size ?? 0 });
        }
      }
    };

    await walk(fullPath, '');
    return items;
  },

  async exists(filePath: string, base: BaseDir): Promise<boolean> {
    const fullPath = await toAbsolute(this.resolvePath(filePath, base));
    try {
      await fsp.access(fullPath);
      return true;
    } catch {
      return false;
    }
  },

  async stats(filePath: string, base: BaseDir): Promise<FileInfo> {
    const fullPath = await toAbsolute(this.resolvePath(filePath, base));
    const stat = await fsp.stat(fullPath);
    return {
      isFile: stat.isFile(),
      isDirectory: stat.isDirectory(),
      size: stat.size,
      mtime: stat.mtime,
      atime: stat.atime,
      birthtime: stat.birthtime,
    };
  },
};

export class NodeAppService extends BaseAppService {
  protected fs = nodeFileSystem;
  override appPlatform = 'node' as const;
  override osPlatform: OsPlatform =
    process.platform === 'darwin'
      ? 'macos'
      : process.platform === 'win32'
        ? 'windows'
        : process.platform === 'linux'
          ? 'linux'
          : 'unknown';

  constructor(customRootDir?: string) {
    super();
    if (customRootDir) {
      this.fs.resolvePath = getPathResolver({ customRootDir: nodePath.resolve(customRootDir) });
    }
  }

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

  override async resolveFilePath(path: string, base: BaseDir): Promise<string> {
    const prefix = await this.fs.getPrefix(base);
    if (!path) return prefix;
    return prefix ? nodePath.join(prefix, path) : path;
  }

  async init(): Promise<void> {
    await this.prepareBooksDir();
  }

  async setCustomRootDir(customRootDir: string): Promise<void> {
    this.fs.resolvePath = getPathResolver({ customRootDir: nodePath.resolve(customRootDir) });
    await this.prepareBooksDir();
  }

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

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

  async saveFile(
    _filename: string,
    content: string | ArrayBuffer | null,
    options?: {
      filePath?: string;
      mimeType?: string;
      share?: boolean;
      sharePosition?: { x: number; y: number; preferredEdge?: 'top' | 'bottom' | 'left' | 'right' };
    },
  ): Promise<boolean> {
    try {
      const filepath = options?.filePath ?? '';
      await fsp.mkdir(nodePath.dirname(filepath), { recursive: true });
      if (typeof content === 'string') {
        await fsp.writeFile(filepath, content, 'utf-8');
      } else if (content) {
        await fsp.writeFile(filepath, Buffer.from(content));
      }
      return true;
    } catch (error) {
      console.error('Failed to save file:', error);
      return false;
    }
  }

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

  async ask(): Promise<boolean> {
    return false;
  }

  async openDatabase(
    schema: SchemaType,
    path: string,
    base: BaseDir,
    opts?: DatabaseOpts,
  ): Promise<DatabaseService> {
    const fullPath = await this.resolveFilePath(path, base);
    const { NodeDatabaseService } = await import('./database/nodeDatabaseService');
    const db = await NodeDatabaseService.open(fullPath, opts);
    const { migrate } = await import('./database/migrate');
    const { getMigrations } = await import('./database/migrations');
    await migrate(db, getMigrations(schema));
    return db;
  }
}
