/*
 *     Created by Angel Leon (@gubatron), Alden Torres (aldenml)
 *     Copyright (c) 2011-2026, FrostWire(R). All rights reserved.
 * 
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package org.team_hermes.wireshare.bittorrent;

import com.frostwire.jlibtorrent.*;
import com.frostwire.jlibtorrent.alerts.*;
import com.frostwire.jlibtorrent.swig.add_torrent_params;
import com.frostwire.jlibtorrent.swig.entry;
import com.frostwire.jlibtorrent.swig.string_entry_map;
import com.frostwire.jlibtorrent.swig.string_vector;
import org.team_hermes.wireshare.platform.Platforms;
import org.team_hermes.wireshare.transfers.BittorrentDownload;
import org.team_hermes.wireshare.transfers.TransferItem;
import org.team_hermes.wireshare.transfers.TransferState;
import org.team_hermes.wireshare.util.Logger;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author gubatron
 * @author aldenml
 */
public final class BTDownload implements BittorrentDownload {
    private static final Logger LOG = Logger.getLogger(BTDownload.class);
    private static final long SAVE_RESUME_RESOLUTION_MILLIS = 10000;
    private static final long STATUS_CACHE_MILLIS = 1000; // Cache status for 1 second to avoid ANR
    private static final int[] ALERT_TYPES = {
            AlertType.TORRENT_FINISHED.swig(),
            AlertType.TORRENT_REMOVED.swig(),
            AlertType.TORRENT_CHECKED.swig(),
            AlertType.SAVE_RESUME_DATA.swig(),
            AlertType.PIECE_FINISHED.swig(),
            AlertType.STORAGE_MOVED.swig()};
    private static final String EXTRA_DATA_KEY = "extra_data";
    private static final String WAS_PAUSED_EXTRA_KEY = "was_paused";
    private final BTEngine engine;
    private final TorrentHandle th;
    private final File savePath;
    private final Date created;
    private final PiecesTracker piecesTracker;
    private final File parts;
    private final Map<String, String> extra;
    private final PaymentOptions paymentOptions;
    private final InnerListener innerListener;
    private BTDownloadListener listener;
    private Set<File> incompleteFilesToRemove;
    private long lastSaveResumeTime;
    private String predominantFileExtension;
    private volatile boolean removing;
    private volatile TorrentStatus cachedStatus;
    private volatile long lastStatusUpdateTime;
    private final AtomicBoolean statusRefreshScheduled = new AtomicBoolean(false);

    public BTDownload(BTEngine engine, TorrentHandle th) {
        this.engine = engine;
        this.th = th;
        this.savePath = new File(th.savePath());
        // Cache the initial status to avoid blocking JNI call during initialization
        TorrentStatus initialStatus = th.status();
        this.created = new Date(initialStatus.addedTime());
        this.cachedStatus = initialStatus;
        this.lastStatusUpdateTime = System.currentTimeMillis();
        TorrentInfo ti = th.torrentFile();
        this.piecesTracker = ti != null ? new PiecesTracker(ti) : null;
        this.parts = ti != null ? createPartsFile(savePath.getAbsolutePath(), ti) : null;
        this.extra = createExtra();
        this.paymentOptions = loadPaymentOptions(ti);
        this.innerListener = new InnerListener();
        engine.addListener(innerListener);
    }

    private static boolean isPaused(TorrentStatus s) {
        return s.flags().and_(TorrentFlags.PAUSED).nonZero();
    }

    public Map<String, String> getExtra() {
        return extra;
    }

    @Override
    public String getName() {
        if (th == null) {
            return null;
        }
        return th.name();
    }

    @Override
    public String getDisplayName() {
        Priority[] priorities = th.filePriorities();
        int count = 0;
        int index = 0;
        for (int i = 0; i < priorities.length; i++) {
            if (!Priority.IGNORE.equals(priorities[i])) {
                count++;
                index = i;
            }
        }
        return count != 1 ? th.name() : FilenameUtils.getName(th.torrentFile().files().filePath(index));
    }

    public long getSize() {
        TorrentInfo ti = th.torrentFile();
        return ti != null ? ti.totalSize() : 0;
    }

    public PaymentOptions getPaymentOptions() {
        return paymentOptions;
    }

    public boolean isPaused() {
        TorrentStatus status = getCachedStatus();
        return status != null && (isPaused(status) || engine.isPausedCached() || !engine.isRunning());
    }

    public boolean isSeeding() {
        TorrentStatus status = getCachedStatus();
        return status != null && status.isSeeding();
    }

    public boolean isFinished() {
        return isFinished(false);
    }

    public boolean isFinished(boolean force) {
        if (!th.isValid()) {
            return false;
        }
        if (force) {
            return th.status(force).isFinished();
        }
        TorrentStatus status = getCachedStatus();
        return status != null && status.isFinished();
    }

    public TransferState getState() {
        if (!engine.isRunning()) {
            return TransferState.STOPPED;
        }
        // Use cached paused state to avoid blocking EDT
        if (engine.isPausedCached()) {
            return TransferState.PAUSED;
        }
        if (!th.isValid()) {
            return TransferState.ERROR;
        }
        TorrentStatus status = getCachedStatus();
        // If cache was invalidated and is null, fetch fresh status
        // This prevents ERROR status from being shown for newly started torrents
        if (status == null && th.isValid()) {
            status = th.status();
            if (status != null) {
                cachedStatus = status;
                lastStatusUpdateTime = System.currentTimeMillis();
            }
        }
        if (status == null) {
            return TransferState.DOWNLOADING;  // Default for new torrents, not ERROR
        }
        final boolean isPaused = isPaused(status);
        if (isPaused && status.isFinished()) {
            return TransferState.FINISHED;
        }
        if (isPaused && !status.isFinished()) {
            return TransferState.PAUSED;
        }
        if (!isPaused && status.isFinished()) { // see the docs of isFinished
            return TransferState.SEEDING;
        }
        final TorrentStatus.State state = status.state();
        switch (state) {
            case CHECKING_FILES:
                return TransferState.CHECKING;
            case DOWNLOADING_METADATA:
                return TransferState.DOWNLOADING_METADATA;
            case DOWNLOADING:
                return TransferState.DOWNLOADING;
            case FINISHED:
                return TransferState.FINISHED;
            case SEEDING:
                return TransferState.SEEDING;
            case CHECKING_RESUME_DATA:
                return TransferState.CHECKING;
            case UNKNOWN:
                return TransferState.UNKNOWN;
            default:
                return TransferState.UNKNOWN;
        }
    }

    /**
     * If we have a torrent which is downloaded as a folder, this will return the parent of that folder.
     * (e.g. default save location for torrents)
     * <p>
     * If you want to have the folder where the torrent's data files are located you
     * want to use getContentSavePath().
     */
    @Override
    public File getSavePath() {
        return savePath;
    }

    @Override
    public File previewFile() {
        return null;
    }

    @Override
    public int getProgress() {
        if (th == null || !th.isValid()) {
            return 0;
        }
        TorrentStatus ts = getCachedStatus();
        if (ts == null) {
            return 0;
        }
        float fp = ts.progress();
        TorrentStatus.State state = ts.state();
        if (Float.compare(fp, 1f) == 0 && state != TorrentStatus.State.CHECKING_FILES) {
            return 100;
        }
        int p = (int) (fp * 100);
        if (p > 0 && state != TorrentStatus.State.CHECKING_FILES) {
            return Math.min(p, 100);
        }
        return 0;
    }

    @Override
    public boolean isComplete() {
        return getProgress() == 100;
    }

    public long getBytesReceived() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.totalDone() : 0;
    }

    public long getTotalBytesReceived() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.allTimeDownload() : 0;
    }

    public long getBytesSent() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.totalUpload() : 0;
    }

    public long getTotalBytesSent() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.allTimeUpload() : 0;
    }

    public long getDownloadSpeed() {
        if (!th.isValid() || isFinished() || isPaused() || isSeeding()) {
            return 0;
        }
        TorrentStatus status = getCachedStatus();
        return status != null ? status.downloadPayloadRate() : 0;
    }

    public long getUploadSpeed() {
        if (!th.isValid() || (isFinished() && !isSeeding()) || isPaused()) {
            return 0;
        }
        TorrentStatus status = getCachedStatus();
        return status != null ? status.uploadPayloadRate() : 0;
    }

    @Override
    public boolean isDownloading() {
        return getDownloadSpeed() > 0;
    }

    /**
     * Gets cached status without blocking on EDT.
     * Returns stale cached data immediately if available, scheduling async refresh if cache is stale.
     * This prevents EDT blocking during rendering by never synchronously calling expensive JNI methods.
     */
    private TorrentStatus getCachedStatus() {
        if (!th.isValid()) {
            return null;
        }
        long now = System.currentTimeMillis();
        long lastUpdate = lastStatusUpdateTime;

        // If cache is still valid, return it immediately (fast path)
        if (cachedStatus != null && (now - lastUpdate) <= STATUS_CACHE_MILLIS) {
            return cachedStatus;
        }

        // Cache is stale or null, return what we have and schedule async refresh
        // This prevents EDT blocking during rendering
        if (statusRefreshScheduled.compareAndSet(false, true)) {
            scheduleStatusRefresh();
        }

        return cachedStatus; // Return stale data (null if no cache yet)
    }

    /**
     * Schedules asynchronous refresh of status cache to avoid blocking operations on EDT.
     */
    private void scheduleStatusRefresh() {
        org.team_hermes.wireshare.concurrent.concurrent.ThreadExecutor.startThread(() -> {
            try {
                if (th.isValid()) {
                    cachedStatus = th.status();
                    lastStatusUpdateTime = System.currentTimeMillis();
                }
            } catch (Exception e) {
                LOG.warn("Error refreshing torrent status cache: " + e.getMessage());
            } finally {
                statusRefreshScheduled.set(false);
            }
        }, "BTDownload-StatusRefresh");
    }

    /**
     * Invalidates the cached status to force a refresh on next access.
     */
    private void invalidateStatusCache() {
        cachedStatus = null;
        lastStatusUpdateTime = 0;
        statusRefreshScheduled.set(false); // Allow immediate refresh on next access
    }

    public int getConnectedPeers() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.numPeers() : 0;
    }

    public TorrentHandle getTorrentHandle() {
        return th;
    }

    public int getTotalPeers() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.listPeers() : 0;
    }

    public int getConnectedSeeds() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.numSeeds() : 0;
    }

    public int getTotalSeeds() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.listSeeds() : 0;
    }

    public long getActiveDuration() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.activeDuration() : 0;
    }

    public long getSeedingDuration() {
        TorrentStatus status = getCachedStatus();
        return status != null ? status.seedingDuration() : 0;
    }

    public boolean isAnnouncingToDht() {
        TorrentStatus status = getCachedStatus();
        return status != null && status.announcingToDht();
    }

    public boolean isAnnouncingToLsd() {
        TorrentStatus status = getCachedStatus();
        return status != null && status.announcingToLsd();
    }

    @Override
    public File getContentSavePath() {
        try {
            if (!th.isValid()) {
                return null;
            }
            TorrentInfo ti = th.torrentFile();
            if (ti != null && ti.swig() != null) {
                return new File(savePath.getAbsolutePath(), ti.numFiles() > 1 ? th.name() : ti.files().filePath(0));
            }
        } catch (Throwable e) {
            LOG.warn("Could not retrieve download content save path", e);
        }
        return null;
    }

    public String getInfoHash() {
        try {
            return th.infoHash().toString().toLowerCase();
        } catch (Throwable t) {
            LOG.error(t.getMessage(), t);
            return null;
        }
    }

    @Override
    public Date getCreated() {
        return created;
    }

    @Override
    public long getETA() {
        if (!th.isValid()) {
            return 0;
        }
        TorrentInfo ti = th.torrentFile();
        if (ti == null) {
            return 0;
        }
        TorrentStatus status = getCachedStatus();
        if (status == null) {
            return 0;
        }
        long left = ti.totalSize() - status.totalDone();
        long rate = status.downloadPayloadRate();
        if (left <= 0) {
            return 0;
        }
        if (rate <= 0) {
            return -1;
        }
        return left / rate;
    }

    public void pause() {
        if (!th.isValid()) {
            return;
        }
        extra.put(WAS_PAUSED_EXTRA_KEY, Boolean.TRUE.toString());
        th.unsetFlags(TorrentFlags.AUTO_MANAGED);
        th.pause();
        invalidateStatusCache();
        doResumeData(true);
    }

    public void resume() {
        if (!th.isValid()) {
            return;
        }
        extra.put(WAS_PAUSED_EXTRA_KEY, Boolean.FALSE.toString());
        th.setFlags(TorrentFlags.AUTO_MANAGED);
        th.resume();
        invalidateStatusCache();
        doResumeData(true);
    }

    public void remove() {
        remove(false, false);
    }

    @Override
    public void remove(boolean deleteData) {
        remove(false, deleteData);
    }

    public void remove(boolean deleteTorrent, boolean deleteData) {
        String infoHash = this.getInfoHash();
        incompleteFilesToRemove = getIncompleteFiles();
        // prevent any further resume serialization during removal
        removing = true;
        engine.removeListener(innerListener);
        if (th.isValid()) {
            if (deleteData) {
                engine.remove(th, SessionHandle.DELETE_FILES);
            } else {
                engine.remove(th);
            }
        }
        if (deleteTorrent) {
            File torrent = engine.readTorrentPath(infoHash);
            if (torrent != null) {
                Platforms.get().fileSystem().delete(torrent);
            }
        }
        // remove any leftover resume/torrent sidecar files under both v1 and v2 hash names
        Set<String> hashesToDelete = new HashSet<>();
        if (infoHash != null) {
            hashesToDelete.add(infoHash);
        }
        try {
            TorrentInfo ti = th.torrentFile();
            if (ti != null) {
                String v1 = null;
                try { v1 = ti.infoHashV1() != null ? ti.infoHashV1().toString().toLowerCase() : null; } catch (Throwable ignore) {}
                if (v1 != null && !v1.isEmpty()) {
                    hashesToDelete.add(v1);
                }
                Sha256Hash v2 = null;
                try { v2 = ti.infoHashV2(); } catch (Throwable ignore) {}
                if (v2 != null && !v2.isAllZeros()) {
                    hashesToDelete.add(v2.toString().toLowerCase());
                }
            }
        } catch (Throwable ignore) { }
        for (String h : hashesToDelete) {
            //noinspection ResultOfMethodCallIgnored
            engine.resumeDataFile(h).delete();
            //noinspection ResultOfMethodCallIgnored
            engine.resumeTorrentFile(h).delete();
        }
    }

    @Override
    public String getPredominantFileExtension() {
        if (predominantFileExtension == null && th != null) {
            TorrentInfo torrentInfo = th.torrentFile();
            if (torrentInfo != null) {
                FileStorage files = torrentInfo.files();
                Map<String, Long> extensionByteSums = new HashMap<>();
                int numFiles = files.numFiles();
                files.paths();
                for (int i = 0; i < numFiles; i++) {
                    String path = files.filePath(i);
                    String extension = FilenameUtils.getExtension(path);
                    if ("".equals(extension)) {
                        // skip folders
                        continue;
                    }
                    if (extensionByteSums.containsKey(extension)) {
                        Long bytes = extensionByteSums.get(extension);
                        extensionByteSums.put(extension, bytes + files.fileSize(i));
                    } else {
                        extensionByteSums.put(extension, files.fileSize(i));
                    }
                }
                String extensionCandidate = null;
                Set<String> exts = extensionByteSums.keySet();
                for (String ext : exts) {
                    if (extensionCandidate == null) {
                        extensionCandidate = ext;
                    } else {
                        Long extSum = extensionByteSums.get(ext);
                        Long extSumCandidate = extensionByteSums.get(extensionCandidate);
                        if (extSum != null && extSumCandidate != null && extSum > extSumCandidate) {
                            extensionCandidate = ext;
                        }
                    }
                }
                predominantFileExtension = extensionCandidate;
            }
        }
        return predominantFileExtension;
    }

    public BTDownloadListener getListener() {
        return listener;
    }

    public void setListener(BTDownloadListener listener) {
        this.listener = listener;
    }

    private void torrentFinished() {
        if (listener != null) {
            try {
                LOG.info("BTDownload::torrentFinished", true);
                listener.finished(this);
            } catch (Throwable e) {
                LOG.error("Error calling listener (finished)", e);
            }
        }
        doResumeData(true);
    }

    private void torrentRemoved() {
        engine.removeListener(innerListener);
        if (parts != null) {
            //noinspection ResultOfMethodCallIgnored
            parts.delete();
        }
        if (listener != null) {
            try {
                listener.removed(this, incompleteFilesToRemove);
            } catch (Throwable e) {
                LOG.error("Error calling listener (removed)", e);
            }
        }
    }

    private void torrentChecked() {
        try {
            if (th.isValid()) {
                // trigger items calculation
                getItems();
            }
        } catch (Throwable e) {
            LOG.warn("Error handling torrent checked logic", e);
        }
    }

    private void pieceFinished(PieceFinishedAlert alert) {
        try {
            if (piecesTracker != null) {
                piecesTracker.setComplete(alert.pieceIndex(), true);
            }
        } catch (Throwable e) {
            LOG.warn("Error handling piece finished logic", e);
        }
    }

    public boolean isPartial() {
        if (th.isValid()) {
            Priority[] priorities = th.filePriorities();
            for (Priority p : priorities) {
                if (Priority.IGNORE.equals(p)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public String magnetUri() {
        return th.makeMagnetUri();
    }

    public int getDownloadRateLimit() {
        return th.getDownloadLimit();
    }

    public void setDownloadRateLimit(int limit) {
        th.setDownloadLimit(limit);
    }

    public int getUploadRateLimit() {
        return th.getUploadLimit();
    }

    public void setUploadRateLimit(int limit) {
        th.setUploadLimit(limit);
    }

    public void requestTrackerAnnounce() {
        th.forceReannounce();
    }

    public void requestTrackerScrape() {
        th.scrapeTracker();
    }

    public void forceRecheck() {
        if (!th.isValid()) {
            return;
        }
        th.forceRecheck();
    }

    public Set<String> trackers() {
        if (!th.isValid()) {
            return new HashSet<>();
        }
        List<AnnounceEntry> trackers = th.trackers();
        Set<String> urls = new HashSet<>(trackers.size());
        for (AnnounceEntry e : trackers) {
            urls.add(e.url());
        }
        return urls;
    }

    public void trackers(Set<String> trackers) {
        List<AnnounceEntry> list = new ArrayList<>(trackers.size());
        for (String url : trackers) {
            list.add(new AnnounceEntry(url));
        }
        th.replaceTrackers(list);
        doResumeData(true);
    }

    @Override
    public List<TransferItem> getItems() {
        ArrayList<TransferItem> items = new ArrayList<>();
        if (th != null && th.isValid()) {
            TorrentInfo ti = th.torrentFile();
            if (ti != null && ti.isValid()) {
                FileStorage fs = ti.files();
                int numFiles = ti.numFiles();
                for (int i = 0; i < numFiles; i++) {
                    BTDownloadItem item = new BTDownloadItem(th, i, fs.filePath(i), fs.fileSize(i), piecesTracker, savePath);
                    items.add(item);
                }
                if (piecesTracker != null) {
                    int numPieces = ti.numPieces();
                    // perform piece complete check
                    for (int i = 0; i < numPieces; i++) {
                        if (th.havePiece(i)) {
                            piecesTracker.setComplete(i, true);
                        }
                    }
                }
            }
        }
        return items;
    }

    public File getTorrentFile() {
        return engine.readTorrentPath(this.getInfoHash());
    }

    public Set<File> getIncompleteFiles() {
        Set<File> s = new HashSet<>();
        try {
            if (!th.isValid()) {
                return s;
            }
            long[] progress = th.fileProgress(TorrentHandle.PIECE_GRANULARITY);
            TorrentInfo ti = th.torrentFile();
            if (ti == null) {
                // still downloading the info (from magnet)
                return s;
            }
            FileStorage fs = ti.files();
            String prefix = savePath.getAbsolutePath();
            long createdTime = created.getTime();
            for (int i = 0; i < progress.length; i++) {
                String fePath = fs.filePath(i);
                long feSize = fs.fileSize(i);
                if (progress[i] < feSize) {
                    // lets see if indeed the file is incomplete
                    File f = new File(prefix, fePath);
                    if (!f.exists()) {
                        continue; // nothing to do here
                    }
                    if (f.lastModified() >= createdTime) {
                        // we have a file modified (supposedly) by this transfer
                        s.add(f);
                    }
                }
            }
        } catch (Throwable e) {
            LOG.error("Error calculating the incomplete files set", e);
        }
        return s;
    }

    public boolean isSequentialDownload() {
        TorrentStatus status = getCachedStatus();
        return status != null && status.flags().and_(TorrentFlags.SEQUENTIAL_DOWNLOAD).eq(TorrentFlags.SEQUENTIAL_DOWNLOAD);
    }

    public void setSequentialDownload(boolean sequential) {
        if (!th.isValid()) {
            System.out.println("BTDownload::setSequentialDownload( " + sequential + ") aborted. Torrent Handle Invalid.");
            return;
        }
        if (sequential) {
            th.setFlags(TorrentFlags.SEQUENTIAL_DOWNLOAD);
        } else {
            th.unsetFlags(TorrentFlags.SEQUENTIAL_DOWNLOAD);
        }
        invalidateStatusCache();
    }

    public File partsFile() {
        return parts;
    }

    private PaymentOptions loadPaymentOptions(TorrentInfo ti) {
        try {
            BTInfoAdditionalMetadataHolder holder = new BTInfoAdditionalMetadataHolder(ti, getDisplayName());
            return holder.getPaymentOptions();
        } catch (Throwable e) {
            return null;
        }
    }

    private void serializeResumeData(SaveResumeDataAlert alert) {
        try {
            if (removing) {
                return;
            }
            if (th.isValid()) {
                String infoHash = th.infoHash().toString();
                File file = engine.resumeDataFile(infoHash);
                entry e = add_torrent_params.write_resume_data(alert.swig().getParams());
                e.dict().put(EXTRA_DATA_KEY, Entry.fromMap(extra).swig());
                FileUtils.writeByteArrayToFile(file, Vectors.byte_vector2bytes(e.bencode()));
            }
        } catch (Throwable e) {
            LOG.warn("Error saving resume data", e);
        }
    }

    private void doResumeData(boolean force) {
        long now = System.currentTimeMillis();
        if (force || (now - lastSaveResumeTime) >= SAVE_RESUME_RESOLUTION_MILLIS) {
            lastSaveResumeTime = now;
        } else {
            // skip, too fast, see SAVE_RESUME_RESOLUTION_MILLIS
            return;
        }
        try {
            if (th != null && th.isValid() && th.needSaveResumeData()) {
                th.saveResumeData(TorrentHandle.ONLY_IF_MODIFIED);
            }
        } catch (Throwable e) {
            LOG.warn("Error triggering resume data", e);
        }
    }

    /**
     * Creates a parts file for the torrent, handling both v1 and v2 torrents.
     * For v2-only torrents, uses the torrent handle's info hash; for others, uses v1 hash.
     */
    private File createPartsFile(String savePath, TorrentInfo ti) {
        try {
            String hash;
            if (ti.infoHashV1() != null) {
                // Use v1 hash for v1-only and hybrid torrents
                hash = ti.infoHashV1().toString();
            } else {
                // For v2-only torrents, use the torrent handle's hash
                hash = th.infoHash().toString();
            }
            return new File(savePath, "." + hash + ".parts");
        } catch (Exception e) {
            LOG.warn("Error creating parts file path: " + e.getMessage());
            // Fallback to using the torrent handle's hash
            try {
                return new File(savePath, "." + th.infoHash().toString() + ".parts");
            } catch (Exception e2) {
                LOG.error("Failed to create parts file path", e2);
                return null;
            }
        }
    }

    private Map<String, String> createExtra() {
        Map<String, String> map = new HashMap<>();
        try {
            String infoHash = getInfoHash();
            File file = engine.resumeDataFile(infoHash);
            if (file.exists()) {
                byte[] arr = FileUtils.readFileToByteArray(file);
                entry e = entry.bdecode(Vectors.bytes2byte_vector(arr));
                string_entry_map d = e.dict();
                if (d.containsKey(EXTRA_DATA_KEY)) {
                    readExtra(d.get(EXTRA_DATA_KEY).dict(), map);
                }
            }
        } catch (Throwable e) {
            LOG.error("Error reading extra data from resume file", e);
        }
        return map;
    }

    private void readExtra(string_entry_map dict, Map<String, String> map) {
        string_vector keys = new string_vector(dict.keySet());
        for (String k : keys) {
            entry e = dict.get(k);
            if (e.type() == entry.data_type.string_t) {
                map.put(k, e.string());
            }
        }
    }

    public boolean wasPaused() {
        boolean flag = false;
        if (extra.containsKey(WAS_PAUSED_EXTRA_KEY)) {
            try {
                flag = Boolean.parseBoolean(extra.get(WAS_PAUSED_EXTRA_KEY));
            } catch (Throwable e) {
                // ignore
            }
        }
        return flag;
    }

    private final class InnerListener implements AlertListener {
        @Override
        public int[] types() {
            return ALERT_TYPES;
        }

        @Override
        public void alert(Alert<?> alert) {
            if (!(alert instanceof TorrentAlert<?>)) {
                return;
            }
            if (!((TorrentAlert<?>) alert).handle().swig().op_eq(th.swig())) {
                return;
            }
            AlertType type = alert.type();
            switch (type) {
                case TORRENT_FINISHED:
                    invalidateStatusCache();
                    torrentFinished();
                    break;
                case TORRENT_REMOVED:
                    invalidateStatusCache();
                    torrentRemoved();
                    break;
                case TORRENT_CHECKED:
                    invalidateStatusCache();
                    torrentChecked();
                    break;
                case SAVE_RESUME_DATA:
                    serializeResumeData((SaveResumeDataAlert) alert);
                    break;
                case PIECE_FINISHED:
                    pieceFinished((PieceFinishedAlert) alert);
                    doResumeData(false);
                    break;
                case STORAGE_MOVED:
                    doResumeData(true);
                    break;
            }
        }
    }
}
