import * as plugins from './cloudflare.plugins.js';
import { logger } from './cloudflare.logger.js';
import * as interfaces from './interfaces/index.js';

// interfaces
import { WorkerManager } from './cloudflare.classes.workermanager.js';
import { ZoneManager } from './cloudflare.classes.zonemanager.js';

export class CloudflareAccount {
  private authToken: string;
  public preselectedAccountId: string;

  public workerManager = new WorkerManager(this);
  public zoneManager = new ZoneManager(this);

  public apiAccount: plugins.cloudflare.Cloudflare;

  /**
   * constructor sets auth information on the CloudflareAccountInstance
   * @param authTokenArg Cloudflare API token
   */
  constructor(authTokenArg: string) {
    this.authToken = authTokenArg;
    this.apiAccount = new plugins.cloudflare.Cloudflare({
      apiToken: this.authToken,
    });
  }

  /**
   * Make a request to the Cloudflare API for endpoints not directly supported by the official client
   * Only use this for endpoints that don't have a direct method in the official client
   * @param method HTTP method (GET, POST, PUT, DELETE)
   * @param endpoint API endpoint path
   * @param data Optional request body data
   * @param customHeaders Optional custom headers to override defaults
   * @returns API response
   */
  public async request<T = any>(
    method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
    endpoint: string,
    data?: any,
    customHeaders?: Record<string, string>
  ): Promise<T> {
    try {
      const options: plugins.smartrequest.ISmartRequestOptions = {
        method,
        headers: {
          'Authorization': `Bearer ${this.authToken}`,
          'Content-Type': 'application/json',
          ...customHeaders,
        },
      };

      if (data) {
        if (customHeaders && customHeaders['Content-Type']?.includes('multipart/form-data')) {
          // For multipart form data, use the data directly as the request body
          options.requestBody = data;
        } else {
          // For JSON requests, stringify the data
          options.requestBody = JSON.stringify(data);
        }
      }

      logger.log('debug', `Making ${method} request to ${endpoint}`);
      const response = await plugins.smartrequest.request(`https://api.cloudflare.com/client/v4${endpoint}`, options);
      
      // Check if response is already an object (might happen with newer smartrequest versions)
      if (typeof response.body === 'object' && response.body !== null) {
        return response.body;
      }
      
      // Otherwise try to parse as JSON
      try {
        if (typeof response.body === 'string' && response.body.trim()) {
          return JSON.parse(response.body);
        } else {
          // If body is empty or not a string, return an empty result
          logger.log('warn', `Empty or invalid response body: ${typeof response.body}`);
          return { result: [] } as T;
        }
      } catch (parseError) {
        logger.log('warn', `Failed to parse response as JSON: ${parseError.message}`);
        
        // Create a fake response object to maintain expected structure
        return {
          result: [],
          success: true,
          errors: [],
          messages: [`Failed to parse: ${typeof response.body === 'string' ? response.body?.substring(0, 50) : typeof response.body}...`]
        } as T;
      }
    } catch (error) {
      logger.log('error', `Cloudflare API request failed: ${error.message}`);
      throw error;
    }
  }

  public async preselectAccountByName(nameArg: string) {
    const accounts = await this.convenience.listAccounts();
    const account = accounts.find((accountArg) => {
      return accountArg.name === nameArg;
    });
    if (account) {
      this.preselectedAccountId = account.id;
    } else {
      throw new Error(`account with name ${nameArg} not found`);
    }
  }

  public convenience = {
    /**
     * Lists all accounts accessible with the current API token
     * @returns Array of Cloudflare account objects
     */
    listAccounts: async () => {
      try {
        const accounts: plugins.ICloudflareTypes['Account'][] = [];
        
        // Collect all accounts using async iterator
        for await (const account of this.apiAccount.accounts.list()) {
          accounts.push(account as interfaces.ICloudflareApiAccountObject);
        }
        
        logger.log('info', `Found ${accounts.length} accounts`);
        return accounts;
      } catch (error) {
        logger.log('error', `Failed to list accounts: ${error.message}`);
        return [];
      }
    },
    /**
     * gets a zone id of a domain from cloudflare
     * @param domainName
     */
    getZoneId: async (domainName: string) => {
      const domain = new plugins.smartstring.Domain(domainName);
      const zoneArray = await this.convenience.listZones(domain.zoneName);
      const filteredResponse = zoneArray.filter((zoneArg) => {
        return zoneArg.name === domainName;
      });
      if (filteredResponse.length >= 1) {
        return filteredResponse[0].id;
      } else {
        logger.log('error', `the domain ${domainName} does not appear to be in this account!`);
        throw new Error(`the domain ${domainName} does not appear to be in this account!`);
      }
    },
    /**
     * gets a record
     * @param domainNameArg
     * @param typeArg
     */
    getRecord: async (
      domainNameArg: string,
      typeArg: plugins.tsclass.network.TDnsRecordType
    ): Promise<plugins.ICloudflareTypes['Record'] | undefined> => {
      try {
        const domain = new plugins.smartstring.Domain(domainNameArg);
        const recordArrayArg = await this.convenience.listRecords(domain.zoneName);
        
        if (!Array.isArray(recordArrayArg)) {
          logger.log('warn', `Expected records array for ${domainNameArg} but got ${typeof recordArrayArg}`);
          return undefined;
        }
        
        const filteredResponse = recordArrayArg.filter((recordArg) => {
          return recordArg.type === typeArg && recordArg.name === domainNameArg;
        });
        
        return filteredResponse.length > 0 ? filteredResponse[0] : undefined;
      } catch (error) {
        logger.log('error', `Error getting record for ${domainNameArg}: ${error.message}`);
        return undefined;
      }
    },
    /**
     * creates a record
     */
    createRecord: async (
      domainNameArg: string,
      typeArg: plugins.tsclass.network.TDnsRecordType,
      contentArg: string,
      ttlArg = 1
    ): Promise<any> => {
      const domain = new plugins.smartstring.Domain(domainNameArg);
      const zoneId = await this.convenience.getZoneId(domain.zoneName);
      const response = await this.apiAccount.dns.records.create({
        zone_id: zoneId,
        type: typeArg as any,
        name: domain.fullName,
        content: contentArg,
        ttl: ttlArg,
      })
      return response;
    },
    /**
     * removes a record from Cloudflare
     * @param domainNameArg
     * @param typeArg
     */
    removeRecord: async (
      domainNameArg: string,
      typeArg: plugins.tsclass.network.TDnsRecordType
    ): Promise<any> => {
      const domain = new plugins.smartstring.Domain(domainNameArg);
      const zoneId = await this.convenience.getZoneId(domain.zoneName);
      const records = await this.convenience.listRecords(domain.zoneName);
      const recordToDelete = records.find((recordArg) => {
        return recordArg.name === domainNameArg && recordArg.type === typeArg;
      });
      if (recordToDelete) {
        // The official client might have the id in a different location
        // Casting to any to access the id property
        const recordId = (recordToDelete as any).id;
        await this.apiAccount.dns.records.delete(recordId, {
          zone_id: zoneId,
        });
      } else {
        logger.log('warn', `record ${domainNameArg} of type ${typeArg} not found`);
      }
    },

    /**
     * cleanrecord allows the cleaning of any previous records to avoid unwanted sideeffects
     */
    cleanRecord: async (domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType) => {
      try {
        logger.log('info', `Cleaning ${typeArg} records for ${domainNameArg}`);
        const domain = new plugins.smartstring.Domain(domainNameArg);
        const zoneId = await this.convenience.getZoneId(domain.zoneName);
        
        // List all records in the zone for this domain
        const records = await this.convenience.listRecords(domain.zoneName);
        
        if (!Array.isArray(records)) {
          logger.log('warn', `Expected records array for ${domainNameArg} but got ${typeof records}`);
          return;
        }
        
        // Only delete records matching the specified name and type
        const recordsToDelete = records.filter((recordArg) => {
          return recordArg.type === typeArg && recordArg.name === domainNameArg;
        });
        
        logger.log('info', `Found ${recordsToDelete.length} ${typeArg} records to delete for ${domainNameArg}`);
        
        for (const recordToDelete of recordsToDelete) {
          try {
            // The official client might have different property locations
            // Casting to any to access properties safely
            const recordId = (recordToDelete as any).id;
            if (!recordId) {
              logger.log('warn', `Record ID not found for ${domainNameArg} record`);
              continue;
            }
            
            await this.apiAccount.dns.records.delete(recordId, {
              zone_id: zoneId,
            });
            logger.log('info', `Deleted ${typeArg} record ${recordId} for ${domainNameArg}`);
          } catch (deleteError) {
            logger.log('error', `Failed to delete record: ${deleteError.message}`);
          }
        }
      } catch (error) {
        logger.log('error', `Error cleaning ${typeArg} records for ${domainNameArg}: ${error.message}`);
      }
    },

    /**
     * updates a record
     * @param domainNameArg Domain name for the record
     * @param typeArg Type of DNS record
     * @param contentArg New content for the record
     * @param ttlArg Time to live in seconds (optional)
     * @returns Updated record
     */
    updateRecord: async (
      domainNameArg: string,
      typeArg: plugins.tsclass.network.TDnsRecordType,
      contentArg: string,
      ttlArg: number = 1
    ): Promise<plugins.ICloudflareTypes['Record']> => {
      const domain = new plugins.smartstring.Domain(domainNameArg);
      const zoneId = await this.convenience.getZoneId(domain.zoneName);
      
      // Find existing record
      const record = await this.convenience.getRecord(domainNameArg, typeArg);
      
      if (!record) {
        logger.log('warn', `Record ${domainNameArg} of type ${typeArg} not found for update, creating instead`);
        return this.convenience.createRecord(domainNameArg, typeArg, contentArg, ttlArg);
      }
      
      // Update the record - cast to any to access the id property
      const recordId = (record as any).id;
      const updatedRecord = await this.apiAccount.dns.records.edit(recordId, {
        zone_id: zoneId,
        type: typeArg as any,
        name: domain.fullName,
        content: contentArg,
        ttl: ttlArg
      });
      
      return updatedRecord;
    },
    /**
     * list all records of a specified domain name
     * @param domainNameArg - the domain name that you want to get the records from
     */
    listRecords: async (domainNameArg: string) => {
      try {
        const domain = new plugins.smartstring.Domain(domainNameArg);
        const zoneId = await this.convenience.getZoneId(domain.zoneName);
        const records: plugins.ICloudflareTypes['Record'][] = [];
        
        // Collect all records using async iterator
        for await (const record of this.apiAccount.dns.records.list({
          zone_id: zoneId,
        })) {
          records.push(record);
        }
        
        logger.log('info', `Found ${records.length} DNS records for ${domainNameArg}`);
        return records;
      } catch (error) {
        logger.log('error', `Failed to list records for ${domainNameArg}: ${error.message}`);
        return [];
      }
    },
    /**
     * list all zones in the associated authenticated account
     * @param domainName optional filter by domain name
     */
    listZones: async (domainName?: string) => {
      try {
        const options: any = {};
        if (domainName) {
          options.name = domainName;
        }
        
        const zones: plugins.ICloudflareTypes['Zone'][] = [];
        
        // Collect all zones using async iterator
        for await (const zone of this.apiAccount.zones.list(options)) {
          zones.push(zone);
        }
        
        logger.log('info', `Found ${zones.length} zones${domainName ? ` matching ${domainName}` : ''}`);
        return zones;
      } catch (error) {
        logger.log('error', `Failed to list zones: ${error.message}`);
        return [];
      }
    },
    /**
     * purges a zone
     */
    purgeZone: async (domainName: string): Promise<void> => {
      const domain = new plugins.smartstring.Domain(domainName);
      const zoneId = await this.convenience.getZoneId(domain.zoneName);
      await this.apiAccount.cache.purge({
        zone_id: zoneId,
        purge_everything: true,
      });
    },

    // acme convenience functions
    acmeSetDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => {
      await this.convenience.cleanRecord(dnsChallenge.hostName, 'TXT');
      await this.convenience.createRecord(
        dnsChallenge.hostName,
        'TXT',
        dnsChallenge.challenge,
        120
      );
    },
    acmeRemoveDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => {
      await this.convenience.removeRecord(dnsChallenge.hostName, 'TXT');
    },
  };
}