import clsx from 'clsx';
import semver from 'semver';
import Image from 'next/image';
import { useEnv } from '@/context/EnvContext';
import { useEffect, useState } from 'react';
import { type as osType, arch as osArch } from '@tauri-apps/plugin-os';
import { check, Update } from '@tauri-apps/plugin-updater';
import { relaunch, exit } from '@tauri-apps/plugin-process';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { Command } from '@tauri-apps/plugin-shell';
import { invoke } from '@tauri-apps/api/core';
import { desktopDir } from '@tauri-apps/api/path';
import { isTauriAppPlatform } from '@/services/environment';
import { useTranslator } from '@/hooks/useTranslator';
import { useTranslation } from '@/hooks/useTranslation';
import { useSearchParams } from 'next/navigation';
import { getAppVersion } from '@/utils/version';
import { tauriDownload } from '@/utils/transfer';
import { installPackage, verifyUpdateSignature, installNightlyUpdate } from '@/utils/bridge';
import { join } from '@tauri-apps/api/path';
import { getLocale } from '@/utils/misc';
import { setLastShownReleaseNotesVersion } from '@/helpers/updater';
import type { ResolvedNightlyUpdate } from '@/helpers/updater';
import {
  READEST_UPDATER_FILE,
  READEST_CHANGELOG_FILE,
  READEST_UPDATER_PUBKEY,
} from '@/services/constants';
import Dialog from '@/components/Dialog';
import Link from './Link';

interface ReleaseNotes {
  releases: Record<
    string,
    {
      date: string;
      notes: string[];
    }
  >;
}

interface Changelog {
  version: string;
  date: string;
  notes: string[];
}

type DownloadEvent =
  | {
      event: 'Started';
      data: {
        contentLength?: number;
      };
    }
  | {
      event: 'Progress';
      data: {
        chunkLength: number;
      };
    }
  | {
      event: 'Finished';
    };

interface GenericUpdate {
  currentVersion: string;
  version: string;
  date?: string;
  body?: string;
  downloadAndInstall?(onEvent?: (progress: DownloadEvent) => void): Promise<void>;
}

const TAURI_UPDATER_KEYS = new Set([
  'darwin-aarch64',
  'darwin-x86_64',
  'windows-x86_64',
  'windows-aarch64',
]);

// Render a nightly stamp (e.g. "0.11.4-2026061406") in a human form. Returns
// the input unchanged for plain semver versions so stable releases display
// normally.
const formatVersionLabel = (v: string): string => {
  const m = v.match(/^(\d+\.\d+\.\d+)-(\d{4})(\d{2})(\d{2})(\d{2})$/);
  if (!m) return v;
  const [, base, y, mo, d, h] = m;
  const date = new Date(Number(y), Number(mo) - 1, Number(d));
  return `Nightly · ${base} (${date.toLocaleDateString()}, ${h}:00)`;
};

export const UpdaterContent = ({
  latestVersion,
  lastVersion,
  checkUpdate = true,
  nightlyUpdate,
}: {
  latestVersion?: string;
  lastVersion?: string;
  checkUpdate?: boolean;
  nightlyUpdate?: ResolvedNightlyUpdate;
}) => {
  const _ = useTranslation();
  const [targetLang, setTargetLang] = useState('EN');
  const { translate } = useTranslator({
    provider: 'azure',
    sourceLang: 'AUTO',
    targetLang,
  });
  const { appService } = useEnv();
  const searchParams = useSearchParams();
  const currentVersion = getAppVersion();
  const [newVersion, setNewVersion] = useState(
    latestVersion ?? searchParams?.get('latestVersion') ?? '',
  );
  const [oldVersion, setOldVersion] = useState(
    lastVersion ?? searchParams?.get('lastVersion') ?? '',
  );
  const [update, setUpdate] = useState<GenericUpdate | Update | null>(null);
  const [changelogs, setChangelogs] = useState<Changelog[]>([]);
  const [progress, setProgress] = useState<number | null>(null);
  const [contentLength, setContentLength] = useState<number | null>(null);
  const [isDownloading, setIsDownloading] = useState(false);
  const [downloaded, setDownloaded] = useState<number | null>(null);
  const [isMounted, setIsMounted] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    setTargetLang(getLocale());
  }, []);

  useEffect(() => {
    const checkDesktopUpdate = async () => {
      const update = await check();
      if (update) {
        setUpdate(update);
      }
    };
    const checkAndroidUpdate = async () => {
      if (!appService) return;
      const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
      const response = await fetch(READEST_UPDATER_FILE);
      const data = await response.json();
      if (semver.gt(data.version, currentVersion)) {
        const OS_ARCH = osArch();
        const platformKey = OS_ARCH === 'aarch64' ? 'android-arm64' : 'android-universal';
        const arch = OS_ARCH === 'aarch64' ? 'arm64' : 'universal';
        const downloadUrl = data.platforms[platformKey]?.url as string;
        const apkFilePath = await appService.resolveFilePath(
          `Readest_${data.version}_${arch}.apk`,
          'Cache',
        );
        setUpdate({
          currentVersion,
          version: data.version,
          date: data.pub_date,
          body: data.notes,
          downloadAndInstall: async (onEvent) => {
            await new Promise<void>(async (resolve, reject) => {
              let downloaded = 0;
              let total = 0;
              await tauriDownload(downloadUrl, apkFilePath, (progress) => {
                if (!onEvent) return;
                if (!total && progress.total) {
                  total = progress.total;
                  onEvent({
                    event: 'Started',
                    data: { contentLength: total },
                  });
                } else if (downloaded > 0 && progress.progress === progress.total) {
                  console.log('APK downloaded to', apkFilePath);
                  onEvent?.({ event: 'Finished' });
                  setTimeout(() => {
                    resolve();
                  }, 1000);
                }

                onEvent({
                  event: 'Progress',
                  data: { chunkLength: progress.progress - downloaded },
                });
                downloaded = progress.progress;
              }).catch((error) => {
                console.error('Download failed:', error);
                reject(error);
              });
            });

            const res = await installPackage({
              path: apkFilePath,
            });
            if (res.success) {
              console.log('APK installed successfully');
            } else {
              console.error('Failed to install APK:', res.error);
            }
          },
        } as GenericUpdate);
      }
    };
    const downloadWithProgress = (
      downloadUrl: string,
      filePath: string,
      onEvent?: (progress: DownloadEvent) => void,
    ): Promise<void> => {
      return new Promise<void>(async (resolve, reject) => {
        let downloaded = 0;
        let total = 0;
        await tauriDownload(downloadUrl, filePath, (progress) => {
          if (!onEvent) return;
          if (!total && progress.total) {
            total = progress.total;
            onEvent({
              event: 'Started',
              data: { contentLength: total },
            });
          } else if (downloaded > 0 && progress.progress === progress.total) {
            console.log('File downloaded to', filePath);
            onEvent?.({ event: 'Finished' });
            setTimeout(() => {
              resolve();
            }, 1000);
          }

          onEvent({
            event: 'Progress',
            data: { chunkLength: progress.progress - downloaded },
          });
          downloaded = progress.progress;
        }).catch((error) => {
          console.error('Download failed:', error);
          reject(error);
        });
      });
    };
    const checkWindowsPortableUpdate = async () => {
      if (!appService) return;
      const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
      const response = await fetch(READEST_UPDATER_FILE);
      const data = await response.json();
      if (semver.gt(data.version, currentVersion)) {
        const OS_ARCH = osArch();
        const platformKey =
          OS_ARCH === 'x86_64' ? 'windows-x86_64-portable' : 'windows-aarch64-portable';
        const arch = OS_ARCH === 'x86_64' ? 'x64' : 'arm64';
        const downloadUrl = data.platforms[platformKey]?.url as string;
        const execDir = await invoke<string>('get_executable_dir');
        const exeFileName = `Readest_${data.version}_${arch}-portable.exe`;
        const exeFilePath = await join(execDir, exeFileName);
        setUpdate({
          currentVersion,
          version: data.version,
          date: data.pub_date,
          body: data.notes,
          downloadAndInstall: async (onEvent) => {
            await downloadWithProgress(downloadUrl, exeFilePath, onEvent);
            try {
              console.log('Launching new executable:', exeFilePath);
              const command = Command.create('start-readest', ['/C', 'start', '', exeFilePath]);
              await command.spawn();
              console.log('New executable launched, exiting current app...');
              setTimeout(async () => {
                await exit(0);
              }, 500);
            } catch (error) {
              console.error('Failed to launch new executable:', error);
            }
          },
        } as GenericUpdate);
      }
    };
    const checkAppImageUpdate = async () => {
      if (!appService) return;
      const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
      const response = await fetch(READEST_UPDATER_FILE);
      const data = await response.json();
      if (semver.gt(data.version, currentVersion)) {
        const OS_ARCH = osArch();
        const platformKey =
          OS_ARCH === 'x86_64' ? 'linux-x86_64-appimage' : 'linux-aarch64-appimage';
        const arch = OS_ARCH === 'x86_64' ? 'x86_64' : 'aarch64';
        const downloadUrl = data.platforms[platformKey]?.url as string;
        const appImageFileName = `Readest_${data.version}_${arch}.AppImage`;
        const appImageFilePath = await join(await desktopDir(), appImageFileName);
        setUpdate({
          currentVersion,
          version: data.version,
          date: data.pub_date,
          body: data.notes,
          downloadAndInstall: async (onEvent) => {
            await downloadWithProgress(downloadUrl, appImageFilePath, onEvent);
            try {
              // Make the AppImage executable
              const chmodCommand = Command.create('chmod-appimage', ['+x', appImageFilePath]);
              await chmodCommand.execute();
              console.log('AppImage made executable:', appImageFilePath);

              // Launch the new AppImage
              console.log('Launching new AppImage:', appImageFilePath);
              const launchCommand = Command.create('launch-appimage', [appImageFilePath]);
              await launchCommand.spawn();
              console.log('New AppImage launched, exiting current app...');
              setTimeout(async () => {
                await exit(0);
              }, 500);
            } catch (error) {
              console.error('Failed to launch new AppImage:', error);
            }
          },
        } as GenericUpdate);
      }
    };
    const buildNightlyUpdate = (n: ResolvedNightlyUpdate): GenericUpdate => ({
      currentVersion,
      version: n.version,
      date: n.pubDate,
      body: n.notes,
      downloadAndInstall: async (onEvent) => {
        if (TAURI_UPDATER_KEYS.has(n.platformKey)) {
          // macOS / Windows-NSIS: Tauri updater (verify + install +
          // relaunch). A 0 contentLength (server omitted Content-Length) is
          // tolerated: we only emit 'Started' once a non-zero total arrives so
          // the percent math never divides by zero.
          let total = 0;
          let lastDownloaded = 0;
          await installNightlyUpdate(n.endpoint, (p) => {
            if (p.event === 'progress') {
              if (!total && p.contentLength) {
                total = p.contentLength;
                onEvent?.({ event: 'Started', data: { contentLength: total } });
              }
              // p.downloaded is a cumulative running total from Rust, but the
              // consumer treats chunkLength as a per-chunk delta, so convert.
              onEvent?.({
                event: 'Progress',
                data: { chunkLength: p.downloaded - lastDownloaded },
              });
              lastDownloaded = p.downloaded;
            } else if (p.event === 'finished') {
              onEvent?.({ event: 'Finished' });
            }
          });
          return;
        }
        // Windows-portable / Linux-AppImage / Android: download, verify, install.
        const fileName = n.url.split('/').pop() || `Readest_${n.version}`;
        let filePath: string;
        if (n.platformKey.includes('portable')) {
          // Windows portable: write into the executable dir so the new exe
          // replaces the running one in place (mirrors checkWindowsPortableUpdate).
          const execDir = await invoke<string>('get_executable_dir');
          filePath = await join(execDir, fileName);
        } else {
          filePath = await appService!.resolveFilePath(fileName, 'Cache');
        }
        await downloadWithProgress(n.url, filePath, onEvent);
        const ok = await verifyUpdateSignature(filePath, n.signature, READEST_UPDATER_PUBKEY);
        if (!ok) {
          console.error('Nightly signature verification failed; aborting install');
          throw new Error('Signature verification failed');
        }
        if (n.platformKey.startsWith('android')) {
          const res = await installPackage({ path: filePath });
          if (!res.success) console.error('Failed to install APK:', res.error);
        } else if (n.platformKey.includes('appimage')) {
          const chmod = Command.create('chmod-appimage', ['+x', filePath]);
          await chmod.execute();
          const launch = Command.create('launch-appimage', [filePath]);
          await launch.spawn();
          setTimeout(async () => {
            await exit(0);
          }, 500);
        } else {
          // windows portable
          const command = Command.create('start-readest', ['/C', 'start', '', filePath]);
          await command.spawn();
          setTimeout(async () => {
            await exit(0);
          }, 500);
        }
      },
    });
    const checkForUpdates = async () => {
      if (nightlyUpdate) {
        setUpdate(buildNightlyUpdate(nightlyUpdate));
        return;
      }
      const OS_TYPE = osType();
      try {
        if (appService?.isPortableApp && OS_TYPE === 'windows') {
          await checkWindowsPortableUpdate();
        } else if (appService?.isAppImage) {
          await checkAppImageUpdate();
        } else if (['macos', 'windows', 'linux'].includes(OS_TYPE)) {
          await checkDesktopUpdate();
        } else if (OS_TYPE === 'android') {
          await checkAndroidUpdate();
        }
      } catch (err) {
        console.error('Failed to check for updates:', err);
        setError(_('Failed to check for updates'));
      }
    };
    if (appService?.hasUpdater && checkUpdate) {
      checkForUpdates();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [appService?.hasUpdater, nightlyUpdate]);

  useEffect(() => {
    if (latestVersion) {
      setNewVersion(latestVersion);
    }
    if (lastVersion) {
      setOldVersion(lastVersion);
    }
    if (!checkUpdate) {
      setUpdate({
        currentVersion,
        version: latestVersion,
        date: '',
        body: '',
      } as GenericUpdate);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [latestVersion, lastVersion, checkUpdate]);

  useEffect(() => {
    const fetchChangelogs = async (fromVersion: string): Promise<Changelog[]> => {
      try {
        const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
        const res = await fetch(READEST_CHANGELOG_FILE);
        const data: ReleaseNotes = await res.json();
        const releases = data.releases;

        let entries = Object.entries(releases)
          .filter(([ver]) => semver.gt(ver, fromVersion))
          .sort(([a], [b]) => semver.rcompare(a, b));
        entries = entries.length ? entries : Object.entries(releases).slice(0, 3);
        return entries.map(([version, info]) => ({
          version,
          date: new Date(info.date).toLocaleDateString(),
          notes: info.notes,
        }));
      } catch (error) {
        console.error('Failed to fetch changelog:', error);
        return [];
      }
    };
    const parseNumberedList = (input: string): string[] => {
      return input
        .split(/(?:^|\s)\d+\.\s/)
        .map((item) => item.trim())
        .filter((item) => item.length > 0);
    };
    const updateChangelogs = async (update: GenericUpdate) => {
      setNewVersion(update.version);
      let changelogs = await fetchChangelogs(oldVersion || currentVersion);
      if (changelogs.length === 0 && update.date && update.body) {
        changelogs = [
          {
            version: update.version,
            date: new Date(update.date).toLocaleDateString(),
            notes: parseNumberedList(update.body ?? ''),
          },
        ];
      }
      if (!targetLang.toLowerCase().startsWith('en')) {
        for (const entry of changelogs) {
          try {
            entry.notes = await translate(entry.notes, { useCache: true });
          } catch (error) {
            console.log('Failed to translate changelog:', error);
          }
        }
      }

      setChangelogs(changelogs);
      setLastShownReleaseNotesVersion(newVersion);
    };
    if (update) {
      updateChangelogs(update);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [update]);

  useEffect(() => {
    setIsMounted(true);
  }, []);

  const handleDownloadInstall = async () => {
    if (!update) {
      return;
    }
    let downloaded = 0;
    let contentLength = 0;
    let lastLogged = 0;
    setProgress(0);
    setIsDownloading(true);
    await update.downloadAndInstall?.((event) => {
      switch (event.event) {
        case 'Started':
          contentLength = event.data.contentLength!;
          setContentLength(contentLength);
          break;
        case 'Progress':
          downloaded += event.data.chunkLength;
          setDownloaded(downloaded);
          // Guard against a 0 total (server omitted Content-Length): keep the
          // bar at an indeterminate 0% instead of NaN/Infinity.
          const percent = contentLength > 0 ? Math.floor((downloaded / contentLength) * 100) : 0;
          setProgress(percent);
          if (downloaded - lastLogged >= 1 * 1024 * 1024) {
            console.log(`downloaded ${downloaded} bytes from ${contentLength}`);
            lastLogged = downloaded;
          }
          break;
        case 'Finished':
          console.log('download finished');
          setProgress(100);
          break;
      }
    });
    console.log('package installed');
    if (!appService?.isAndroidApp && process.env.NODE_ENV === 'production') {
      await relaunch();
    }
  };

  if (!isMounted) {
    return null;
  }

  if (error) {
    return (
      <div className='bg-base-100 flex min-h-screen items-center justify-center'>
        <p className='text-base-content text-sm font-bold'>{error}</p>
      </div>
    );
  }

  if (!newVersion) {
    return null;
  }

  return (
    <div className='bg-base-100 flex min-h-screen justify-center'>
      <div className='flex w-full max-w-2xl flex-col gap-4'>
        <div className='flex flex-col justify-center gap-4 sm:flex-row sm:items-start'>
          <div className='flex items-center justify-center'>
            <Image src='/icon.png' alt='Logo' className='h-20 w-20' width={64} height={64} />
          </div>

          {checkUpdate ? (
            <div className='text-base-content flex-grow text-sm'>
              <h2 className='mb-4 text-center font-bold sm:text-start'>
                {_('A new version of Readest is available!')}
              </h2>
              <p className='mb-2'>
                {_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', {
                  newVersion: formatVersionLabel(newVersion),
                  currentVersion,
                })}
              </p>
              <p className='mb-2'>{_('Download and install now?')}</p>

              <div className='flex w-full flex-row items-center justify-end gap-4'>
                {progress !== null && (
                  <div className='flex flex-grow flex-col'>
                    <progress
                      className='progress my-1 h-4 w-full'
                      value={progress}
                      max='100'
                    ></progress>
                    <p className='text-base-content/75 flex items-center justify-center text-sm'>
                      {progress < 100
                        ? _('Downloading {{downloaded}} of {{contentLength}}', {
                            downloaded: downloaded
                              ? `${Math.floor(downloaded / 1024 / 1024)} MB`
                              : '0 MB',
                            contentLength: contentLength
                              ? `${Math.floor(contentLength / 1024 / 1024)} MB`
                              : '0 MB',
                          })
                        : _('Download finished')}
                    </p>
                  </div>
                )}

                <div className={clsx('card-actions', isDownloading && 'hidden sm:flex')}>
                  <button
                    className={clsx(
                      'btn btn-warning text-base-100 px-6 font-bold',
                      (!update || isDownloading) && 'btn-disabled',
                    )}
                    onClick={handleDownloadInstall}
                  >
                    {_('DOWNLOAD & INSTALL')}
                  </button>
                </div>
              </div>
            </div>
          ) : (
            <div className='text-base-content flex h-full flex-grow flex-col text-sm sm:flex-row'>
              <div className='flex flex-col items-center justify-center gap-4 p-1 sm:items-start sm:gap-2'>
                <h2 className='text-center font-bold sm:text-start'>
                  {_('Version {{version}}', { version: currentVersion })}
                </h2>

                {changelogs.length > 0 && semver.gt(changelogs[0]!.version, currentVersion) ? (
                  <div className='flex gap-2'>
                    {(appService?.isIOSApp || appService?.isMacOSApp) && (
                      <Link
                        href='https://apps.apple.com/app/id6738622779'
                        target='_blank'
                        rel='noopener noreferrer'
                        className='btn btn-primary btn-sm'
                      >
                        {_('Check Update')}
                      </Link>
                    )}

                    {appService?.isAndroidApp && (
                      <Link
                        href='https://play.google.com/store/apps/details?id=com.bilingify.readest'
                        target='_blank'
                        rel='noopener noreferrer'
                        className='btn btn-primary btn-sm'
                      >
                        {_('Check Update')}
                      </Link>
                    )}
                  </div>
                ) : (
                  <div className='flex'>
                    <p className='text-sm font-bold'>{_('Already the latest version')}</p>
                  </div>
                )}
              </div>
            </div>
          )}
        </div>
        <div className='text-base-content text-sm'>
          <h3 className='mb-2 font-bold'>{_('Changelog')}</h3>
          <div className='not-eink:bg-base-200 not-eink:px-4 mb-4 rounded-lg pb-2 pt-4'>
            {changelogs.length > 0 ? (
              changelogs.map((entry: Changelog) => (
                <div key={entry.version} className='mb-4'>
                  <h4 className='mb-2 font-bold'>
                    {entry.version} ({entry.date})
                  </h4>
                  <ul className='list-disc space-y-1 ps-6 text-sm'>
                    {entry.notes.map((note: string, i: number) => (
                      <li key={i}>{note}</li>
                    ))}
                  </ul>
                </div>
              ))
            ) : (
              <div className='flex h-56 w-full flex-col gap-4'>
                <div className='skeleton h-4 w-28'></div>
                <div className='skeleton h-4 w-full'></div>
                <div className='skeleton h-4 w-full'></div>
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

export const setUpdaterWindowVisible = (
  visible: boolean,
  latestVersion: string,
  lastVersion?: string,
  checkUpdate = true,
  nightlyUpdate?: ResolvedNightlyUpdate,
) => {
  const dialog = document.getElementById('updater_window');
  if (dialog) {
    const event = new CustomEvent('setDialogVisibility', {
      detail: { visible, latestVersion, lastVersion, checkUpdate, nightlyUpdate },
    });
    dialog.dispatchEvent(event);
  }
};

export const UpdaterWindow = () => {
  const _ = useTranslation();
  const [latestVersion, setLatestVersion] = useState('');
  const [lastVersion, setLastVersion] = useState('');
  const [checkUpdate, setCheckUpdate] = useState(true);
  const [nightlyUpdate, setNightlyUpdate] = useState<ResolvedNightlyUpdate | undefined>(undefined);
  const [isOpen, setIsOpen] = useState(false);

  useEffect(() => {
    const handleCustomEvent = (event: CustomEvent) => {
      const { visible, latestVersion, lastVersion, checkUpdate, nightlyUpdate } = event.detail;
      setIsOpen(visible);
      setCheckUpdate(checkUpdate);
      setNightlyUpdate(nightlyUpdate);
      if (latestVersion) {
        setLatestVersion(latestVersion);
      }
      if (lastVersion) {
        setLastVersion(lastVersion);
      }
    };

    const el = document.getElementById('updater_window');
    if (el) {
      el.addEventListener('setDialogVisibility', handleCustomEvent as EventListener);
    }

    return () => {
      if (el) {
        el.removeEventListener('setDialogVisibility', handleCustomEvent as EventListener);
      }
    };
  }, []);

  return (
    <Dialog
      id='updater_window'
      isOpen={isOpen}
      title={checkUpdate ? _('Software Update') : _("What's New in Readest")}
      onClose={() => setIsOpen(false)}
      boxClassName='sm:!w-[75%] sm:h-auto sm:!max-h-[85vh] sm:!max-w-2xl'
    >
      {isOpen && (
        <UpdaterContent
          latestVersion={latestVersion ?? undefined}
          lastVersion={lastVersion ?? undefined}
          checkUpdate={checkUpdate}
          nightlyUpdate={nightlyUpdate}
        />
      )}
    </Dialog>
  );
};
