import express, { NextFunction, Request, Response } from 'express';
import { decodePositionId } from 'kraiken-lib/ids';
import { isPositionDelinquent } from 'kraiken-lib/staking';
import { pathToFileURL } from 'url';
import { BotConfigService } from './services/BotConfigService.js';
import { BlockchainService } from './services/BlockchainService.js';
import { GraphQLService } from './services/GraphQLService.js';
import { logger } from './logger.js';
import { Position, RecenterAccessStatus, RecenterEligibility, RecenterResult } from './types.js';

const ACTIVE_POSITIONS_QUERY = `
  query ActivePositions {
    positionss(where: { status: "Active" }, limit: 1000) {
      items {
        id
        share
        lastTaxTime
        taxRate
        status
      }
    }
  }
`;

export interface TxnBotDependencies {
  configService: BotConfigService;
  blockchainService: BlockchainService;
  graphQLService: GraphQLService;
}

export interface TxnBotInstance {
  app: express.Application;
  start: () => Promise<void>;
  getRecenterAccessStatus: (forceRefresh?: boolean) => Promise<RecenterAccessStatus>;
  evaluateRecenterOpportunity: () => Promise<RecenterEligibility>;
  attemptRecenter: () => Promise<RecenterResult>;
}

function resolvePort(value: string): number | string {
  const trimmed = value.trim();
  if (trimmed.length === 0) {
    return 3000;
  }
  const parsed = Number.parseInt(trimmed, 10);
  return Number.isNaN(parsed) || parsed.toString() !== trimmed ? value : parsed;
}

function extractRevertReason(error: unknown): string | null {
  const err = error as {
    shortMessage?: string;
    reason?: string;
    info?: { error?: { message?: string } };
    error?: { message?: string };
    message?: string;
  };

  const candidates = [err?.shortMessage, err?.reason, err?.info?.error?.message, err?.error?.message, err?.message];

  for (const candidate of candidates) {
    if (typeof candidate !== 'string' || candidate.length === 0) {
      continue;
    }
    const match = candidate.match(/execution reverted(?: due to)?(?::|: )?(.*)/i);
    if (match && match[1]) {
      return match[1].trim();
    }
    if (!candidate.toLowerCase().startsWith('execution reverted')) {
      return candidate.trim();
    }
  }
  return null;
}

function formatDuration(ms: number): string {
  let seconds = Math.floor(ms / 1000);
  let minutes = Math.floor(seconds / 60);
  let hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  seconds = seconds % 60;
  minutes = minutes % 60;
  hours = hours % 24;

  return `${days} days, ${hours} hours, ${minutes} minutes`;
}

export function createTxnBot(dependencies: TxnBotDependencies): TxnBotInstance {
  const { configService, blockchainService, graphQLService } = dependencies;
  const envConfig = configService.getConfig();

  const startTime = new Date();
  let lastRecenterTime: Date | null = null;
  let lastLiquidationTime: Date | null = null;
  let lastRecenterTx: string | null = null;
  let lastRecenterAccessStatus: RecenterAccessStatus | null = null;
  let lastRecenterEligibility: RecenterEligibility = {
    checkedAtMs: 0,
    canRecenter: false,
    reason: null,
    error: null,
  };

  async function fetchActivePositions(): Promise<Position[]> {
    return graphQLService.fetchActivePositions(ACTIVE_POSITIONS_QUERY);
  }

  async function checkFunds(): Promise<string> {
    return blockchainService.checkFunds();
  }

  async function getRecenterAccessStatus(forceRefresh = false): Promise<RecenterAccessStatus> {
    const now = Date.now();
    if (
      !forceRefresh &&
      lastRecenterAccessStatus &&
      lastRecenterAccessStatus.checkedAtMs &&
      now - lastRecenterAccessStatus.checkedAtMs < 30000
    ) {
      return lastRecenterAccessStatus;
    }

    lastRecenterAccessStatus = {
      hasAccess: true,
      checkedAtMs: now,
      error: null,
    };

    return lastRecenterAccessStatus;
  }

  async function evaluateRecenterOpportunity(): Promise<RecenterEligibility> {
    const now = Date.now();
    try {
      await blockchainService.estimateRecenterGas();
      lastRecenterEligibility = {
        checkedAtMs: now,
        canRecenter: true,
        reason: null,
        error: null,
      };
    } catch (error) {
      const err = error as { shortMessage?: string; message?: string };
      lastRecenterEligibility = {
        checkedAtMs: now,
        canRecenter: false,
        reason: extractRevertReason(error) || 'recenter not currently executable.',
        error: err?.shortMessage || err?.message || null,
      };
    }

    return lastRecenterEligibility;
  }

  async function attemptRecenter(): Promise<RecenterResult> {
    const eligibility = await evaluateRecenterOpportunity();
    if (!eligibility.canRecenter) {
      return {
        executed: false,
        message: eligibility.reason || 'Liquidity manager denied recenter.',
        eligibility,
      };
    }

    const tx = await blockchainService.recenter();
    lastRecenterTx = tx.hash;
    await tx.wait();
    lastRecenterTime = new Date();
    return {
      executed: true,
      txHash: tx.hash,
      message: 'recenter transaction submitted',
      eligibility: lastRecenterEligibility,
    };
  }

  async function checkPosition(position: Position): Promise<number> {
    const taxRate = Number(position.taxRate);
    const lastTaxTime = Number(position.lastTaxTime);
    if (isPositionDelinquent(lastTaxTime, taxRate)) {
      const positionId = decodePositionId(position.id);
      logger.info(`Calling payTax on ${positionId}`);
      const tx = await blockchainService.payTax(positionId);
      await tx.wait();
      lastLiquidationTime = new Date();
      return 1;
    }
    return 0;
  }

  async function liquidityLoop(): Promise<void> {
    try {
      const result = await attemptRecenter();
      if (result.executed) {
        logger.info(`recenter called successfully. tx=${result.txHash}`);
      } else {
        const reason = result.message ? `Reason: ${result.message}` : 'Reason: unavailable';
        logger.info(`Recenter skipped. ${reason} - ${new Date().toISOString()}`);
      }
    } catch (error) {
      logger.error('Error in liquidity loop', { error });
    }
  }

  async function liquidationLoop(): Promise<void> {
    let counter = 0;
    try {
      const positions = await fetchActivePositions();
      for (const position of positions) {
        counter = counter + (await checkPosition(position));
      }
      if (counter === 0) {
        logger.info(`No tax can be claimed at the moment. - ${new Date().toISOString()}`);
      }
    } catch (error) {
      logger.error('Error in liquidation loop', { error });
    }
  }

  const app = express();

  app.use((req: Request, res: Response, next: NextFunction) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
    if (req.method === 'OPTIONS') {
      return res.sendStatus(204);
    }
    return next();
  });

  app.get('/status', async (_req: Request, res: Response) => {
    try {
      const balance = await checkFunds();
      const uptime = formatDuration(new Date().getTime() - startTime.getTime());
      const recenterAccessStatus = await getRecenterAccessStatus();
      const recenterEligibility = lastRecenterEligibility;
      const status = {
        balance: `${balance} ETH`,
        uptime,
        lastRecenterTime: lastRecenterTime ? lastRecenterTime.toISOString() : 'Never',
        lastLiquidationTime: lastLiquidationTime ? lastLiquidationTime.toISOString() : 'Never',
        lastRecenterTx,
        recenterAccess: {
          hasAccess: recenterAccessStatus?.hasAccess ?? true,
          checkedAt: recenterAccessStatus?.checkedAtMs ? new Date(recenterAccessStatus.checkedAtMs).toISOString() : null,
          error: recenterAccessStatus?.error ?? null,
        },
        recenterEligibility: {
          canRecenter: recenterEligibility?.canRecenter ?? null,
          reason: recenterEligibility?.reason ?? null,
          checkedAt: recenterEligibility?.checkedAtMs ? new Date(recenterEligibility.checkedAtMs).toISOString() : null,
          error: recenterEligibility?.error ?? null,
        },
      };

      if (parseFloat(balance) < 0.1) {
        res.status(500).send(`Low Ethereum Balance: ${balance} ETH`);
      } else {
        res.status(200).json(status);
      }
    } catch (error) {
      const err = error as { message?: string };
      res.status(500).send(`Error checking funds: ${err.message}`);
    }
  });

  app.post('/recenter', async (_req: Request, res: Response) => {
    try {
      const result = await attemptRecenter();
      if (!result.executed) {
        return res.status(202).json(result);
      }
      return res.status(200).json(result);
    } catch (error) {
      const err = error as { message?: string };
      logger.error('Manual recenter failed', { error });
      return res.status(500).json({ error: err.message || 'recenter failed' });
    }
  });

  async function start(): Promise<void> {
    logger.info(`txnBot service started for environment ${envConfig.ENVIRONMENT}`);

    await liquidityLoop();
    await liquidationLoop();
    setInterval(() => {
      void liquidityLoop();
    }, 3 * 60000);
    setInterval(() => {
      void liquidationLoop();
    }, 20 * 60000);

    const port = resolvePort(configService.getPort());
    app.listen(port, () => {
      logger.info(`HTTP server running on port ${configService.getPort()}`);
    });
  }

  return {
    app,
    start,
    getRecenterAccessStatus,
    evaluateRecenterOpportunity,
    attemptRecenter,
  };
}

export async function bootstrap(): Promise<void> {
  const configService = new BotConfigService();
  const blockchainService = new BlockchainService({
    providerUrl: configService.getProviderUrl(),
    privateKey: configService.getPrivateKey(),
    liquidityManagerAddress: configService.getLiquidityManagerAddress(),
    stakeContractAddress: configService.getStakeContractAddress(),
  });
  const graphQLService = new GraphQLService(configService.getGraphQLEndpoint());

  const bot = createTxnBot({ configService, blockchainService, graphQLService });
  await bot.start();
}

const isDirectRun = typeof process.argv[1] === 'string' && pathToFileURL(process.argv[1]).href === import.meta.url;

if (isDirectRun) {
  bootstrap().catch(error => {
    logger.error('Fatal error', { error });
  });
}
