import axios, { AxiosInstance } from "axios";
import { CookieJar } from "tough-cookie";
import { wrapper } from "axios-cookiejar-support";
import FormData from "form-data";
import fs from "node:fs";
import { config } from "../config"
import { logger } from "../utils/logger"
import {
  QbitCategory,
  QbitPeerList,
  QbitTorrentInfo,
  QbitTorrentProperties,
  QbitTransferInfo,
} from "./qbit.types"

export class QbitClient {
  private client: AxiosInstance;
  private jar: CookieJar;
  private loggedIn = false;

  constructor() {
    this.jar = new CookieJar();
    this.client = wrapper(
      axios.create({
        baseURL: config.qbitBaseUrl,
        jar: this.jar,
        withCredentials: true,
      })
    );
  }

  async login(): Promise<void> {
    if (!config.qbitBaseUrl) {
      throw new Error("QBIT_BASE_URL missing");
    }
    const form = new URLSearchParams();
    form.append("username", config.qbitUsername);
    form.append("password", config.qbitPassword);
    await this.client.post("/api/v2/auth/login", form, {
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
    });
    this.loggedIn = true;
  }

  private async request<T>(fn: () => Promise<T>): Promise<T> {
    try {
      if (!this.loggedIn) {
        await this.login();
      }
      return await fn();
    } catch (error) {
      if (axios.isAxiosError(error)) {
        const code = error.code ?? "";
        const transient =
          code === "EAI_AGAIN" ||
          code === "ENOTFOUND" ||
          code === "ECONNREFUSED" ||
          code === "ECONNRESET" ||
          code === "ETIMEDOUT";
        if (transient) {
          this.loggedIn = false;
        }
      }
      if (
        axios.isAxiosError(error) &&
        (error.response?.status === 401 || error.response?.status === 403)
      ) {
        logger.warn("qBittorrent session expired, re-login");
        this.loggedIn = false;
        await this.login();
        return await fn();
      }
      throw error;
    }
  }

  async getVersion(): Promise<string> {
    const response = await this.request(() =>
      this.client.get<string>("/api/v2/app/version")
    );
    return response.data;
  }

  async getTorrentsInfo(): Promise<QbitTorrentInfo[]> {
    const response = await this.request(() =>
      this.client.get<QbitTorrentInfo[]>("/api/v2/torrents/info", {
        params: { filter: "all" },
      })
    );
    return response.data;
  }

  async getTransferInfo(): Promise<QbitTransferInfo> {
    const response = await this.request(() =>
      this.client.get<QbitTransferInfo>("/api/v2/transfer/info")
    );
    return response.data;
  }

  async getCategories(): Promise<Record<string, QbitCategory>> {
    const response = await this.request(() =>
      this.client.get<Record<string, QbitCategory>>("/api/v2/torrents/categories")
    );
    return response.data;
  }

  async getTorrentProperties(hash: string): Promise<QbitTorrentProperties> {
    const response = await this.request(() =>
      this.client.get<QbitTorrentProperties>("/api/v2/torrents/properties", {
        params: { hash },
      })
    );
    return response.data;
  }

  async getTorrentPeers(hash: string): Promise<QbitPeerList> {
    const response = await this.request(() =>
      this.client.get<QbitPeerList>("/api/v2/sync/torrentPeers", {
        params: { hash },
      })
    );
    return response.data;
  }

  async exportTorrent(hash: string): Promise<Buffer> {
    const response = await this.request(() =>
      this.client.get<ArrayBuffer>("/api/v2/torrents/export", {
        params: { hashes: hash },
        responseType: "arraybuffer",
      })
    );
    return Buffer.from(response.data);
  }

  async addTorrentByMagnet(magnet: string, options: Record<string, string> = {}) {
    const form = new URLSearchParams();
    form.append("urls", magnet);
    Object.entries(options).forEach(([key, value]) => form.append(key, value));
    await this.request(() =>
      this.client.post("/api/v2/torrents/add", form, {
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
      })
    );
  }

  async addTorrentByFile(filePath: string, options: Record<string, string> = {}) {
    const form = new FormData();
    form.append("torrents", fs.createReadStream(filePath));
    Object.entries(options).forEach(([key, value]) => form.append(key, value));
    await this.request(() =>
      this.client.post("/api/v2/torrents/add", form, {
        headers: form.getHeaders(),
      })
    );
  }

  async deleteTorrent(hash: string, deleteFiles = true) {
    const form = new URLSearchParams();
    form.append("hashes", hash);
    form.append("deleteFiles", deleteFiles ? "true" : "false");
    await this.request(() =>
      this.client.post("/api/v2/torrents/delete", form, {
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
      })
    );
  }

  async pauseTorrent(hash: string) {
    const form = new URLSearchParams();
    form.append("hashes", hash);
    await this.request(() =>
      this.client.post("/api/v2/torrents/pause", form, {
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
      })
    );
  }

  async resumeTorrent(hash: string) {
    const form = new URLSearchParams();
    form.append("hashes", hash);
    await this.request(() =>
      this.client.post("/api/v2/torrents/resume", form, {
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
      })
    );
  }

  async banPeers(peers: string[]) {
    const form = new URLSearchParams();
    form.append("peers", peers.join("|"));
    await this.request(() =>
      this.client.post("/api/v2/transfer/banPeers", form, {
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
      })
    );
  }
}
