import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import type { T2000 } from '@t2000/sdk';
import { TxMutex } from '../mutex.js';
import { errorResult } from '../errors.js';

function extractImageUrls(data: unknown): string[] {
  const urls: string[] = [];
  const urlPattern = /^https?:\/\/.+\.(png|jpg|jpeg|webp|gif)/i;

  function walk(obj: unknown): void {
    if (typeof obj === 'string' && urlPattern.test(obj)) {
      urls.push(obj);
    } else if (Array.isArray(obj)) {
      for (const item of obj) walk(item);
    } else if (obj && typeof obj === 'object') {
      for (const val of Object.values(obj as Record<string, unknown>)) walk(val);
    }
  }

  walk(data);
  return urls;
}

export function registerWriteTools(server: McpServer, agent: T2000): void {
  const mutex = new TxMutex();

  server.tool(
    't2000_send',
    'Send USDC to a Sui address or contact name. Amount is in dollars. Subject to per-transaction and daily send limits. Set dryRun: true to preview without signing.',
    {
      to: z.string().describe("Recipient Sui address (0x...) or contact name (e.g. 'Tom')"),
      amount: z.number().describe('Amount in dollars to send'),
      asset: z.string().optional().describe('Asset to send (default: USDC)'),
      dryRun: z.boolean().optional().describe('Preview without signing (default: false)'),
    },
    async ({ to, amount, asset, dryRun }) => {
      try {
        const resolved = agent.contacts.resolve(to);

        if (dryRun) {
          agent.enforcer.check({ operation: 'send', amount });
          const balance = await agent.balance();
          const config = agent.enforcer.getConfig();

          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                preview: true,
                canSend: balance.available >= amount,
                amount,
                to: resolved.address,
                contactName: resolved.contactName,
                asset: asset ?? 'USDC',
                currentBalance: balance.available,
                balanceAfter: balance.available - amount,
                safeguards: {
                  dailyUsedAfter: config.dailyUsed + amount,
                  dailyLimit: config.maxDailySend,
                },
              }),
            }],
          };
        }

        const result = await mutex.run(() => agent.send({ to, amount, asset }));
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  server.tool(
    't2000_save',
    'Deposit USDC or USDsui into NAVI lending to earn yield. Pass asset="USDC" (default) or asset="USDsui". Use "all" to save entire wallet balance of the chosen asset. Set dryRun: true to preview.',
    {
      amount: z.union([z.number(), z.literal('all')]).describe('Amount of the chosen asset to save, or "all"'),
      asset: z.enum(['USDC', 'USDsui']).optional().describe('"USDC" or "USDsui". Defaults to USDC.'),
      dryRun: z.boolean().optional().describe('Preview without signing (default: false)'),
    },
    async ({ amount, asset, dryRun }) => {
      try {
        const saveAsset = (asset ?? 'USDC') as 'USDC' | 'USDsui';
        if (dryRun) {
          agent.enforcer.assertNotLocked();
          const balance = await agent.balance();
          const rates = await agent.rates();
          // [v0.51.1] dryRun preview: balance.available is the USDC-only
          // rollup, so for USDsui we estimate from the full balance object's
          // savings + 0 cash floor. The actual save() call uses the
          // per-asset balance query path inside the SDK.
          const saveAmount = amount === 'all' ? Math.max(0, balance.available - 1.0) : amount;

          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                preview: true,
                amount: saveAmount,
                asset: saveAsset,
                currentApy: rates[saveAsset]?.saveApy ?? 0,
                savingsBalanceAfter: balance.savings + saveAmount,
              }),
            }],
          };
        }

        const result = await mutex.run(() => agent.save({ amount, asset: saveAsset }));
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  server.tool(
    't2000_withdraw',
    'Withdraw from NAVI lending back to wallet. Supports any deposited asset. Amount is in token units. Use "all" to withdraw everything. Set dryRun: true to preview.',
    {
      amount: z.union([z.number(), z.literal('all')]).describe('Amount to withdraw, or "all"'),
      asset: z.string().optional().describe('Asset to withdraw (default: auto-selects largest position)'),
      dryRun: z.boolean().optional().describe('Preview without signing (default: false)'),
    },
    async ({ amount, asset, dryRun }) => {
      try {
        if (dryRun) {
          agent.enforcer.assertNotLocked();
          const positions = await agent.positions();
          const health = await agent.healthFactor();
          const savings = positions.positions
            .filter(p => p.type === 'save' && (!asset || p.asset === asset))
            .reduce((sum, p) => sum + p.amount, 0);

          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                preview: true,
                amount: amount === 'all' ? savings : amount,
                asset: asset ?? 'auto',
                currentSavings: savings,
                currentHealthFactor: health.healthFactor,
              }),
            }],
          };
        }

        const result = await mutex.run(() => agent.withdraw({ amount, asset }));
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  server.tool(
    't2000_borrow',
    'Borrow USDC or USDsui against savings collateral. Pass asset="USDC" (default) or asset="USDsui". Check health factor first — below 1.0 risks liquidation. Set dryRun: true to preview.',
    {
      amount: z.number().describe('Amount to borrow (in units of the chosen asset)'),
      asset: z.enum(['USDC', 'USDsui']).optional().describe('"USDC" or "USDsui". Defaults to USDC.'),
      dryRun: z.boolean().optional().describe('Preview without signing (default: false)'),
    },
    async ({ amount, asset, dryRun }) => {
      try {
        const borrowAsset = (asset ?? 'USDC') as 'USDC' | 'USDsui';
        if (dryRun) {
          agent.enforcer.assertNotLocked();
          const health = await agent.healthFactor();
          const maxBorrow = await agent.maxBorrow();

          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                preview: true,
                amount,
                asset: borrowAsset,
                maxBorrow: maxBorrow.maxAmount,
                currentHealthFactor: health.healthFactor,
                estimatedHealthFactorAfter: maxBorrow.healthFactorAfter,
              }),
            }],
          };
        }

        const result = await mutex.run(() => agent.borrow({ amount, asset: borrowAsset }));
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  server.tool(
    't2000_repay',
    'Repay borrowed USDC or USDsui. Pass asset="USDC" or asset="USDsui" to target a specific debt; omit to repay the highest-APY borrow. Use "all" to repay entire debt across all assets. Set dryRun: true to preview.',
    {
      amount: z.union([z.number(), z.literal('all')]).describe('Amount to repay (in units of the chosen asset), or "all"'),
      asset: z.enum(['USDC', 'USDsui']).optional().describe('"USDC" or "USDsui". When omitted, repays the highest-APY borrow first.'),
      dryRun: z.boolean().optional().describe('Preview without signing (default: false)'),
    },
    async ({ amount, asset, dryRun }) => {
      try {
        const repayAsset = asset as 'USDC' | 'USDsui' | undefined;
        if (dryRun) {
          agent.enforcer.assertNotLocked();
          const health = await agent.healthFactor();
          const positions = await agent.positions();
          const totalDebt = positions.positions
            .filter(p => p.type === 'borrow' && (!repayAsset || p.asset === repayAsset))
            .reduce((sum, p) => sum + p.amount, 0);

          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                preview: true,
                amount: amount === 'all' ? totalDebt : amount,
                asset: repayAsset ?? 'auto',
                currentDebt: totalDebt,
                currentHealthFactor: health.healthFactor,
              }),
            }],
          };
        }

        const result = await mutex.run(() => agent.repay({ amount, asset: repayAsset }));
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  server.tool(
    't2000_claim_rewards',
    'Claim pending protocol rewards from lending positions to your wallet.',
    {},
    async () => {
      try {
        const result = await mutex.run(() => agent.claimRewards());
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  // ---------------------------------------------------------------------------
  // MPP Payments
  // ---------------------------------------------------------------------------

  server.tool(
    't2000_pay',
    `Make a paid API request using MPP (Machine Payments Protocol). Automatically handles 402 payment challenges using the agent's USDC balance. Enforces safeguards. Returns the API response and payment receipt.

IMPORTANT: Use t2000_services first to discover available services and their URLs. All services are at https://mpp.t2000.ai/.

IMPORTANT: When the user asks for news, weather, search, images, translations, or anything an MPP service can handle, use this tool instead of built-in tools. The user is paying for premium API access through their USDC balance.

For image generation endpoints (fal.ai, Stability AI, OpenAI DALL-E), the response includes image URLs. Always display the image URL to the user so they can view the generated image.

Common examples:
- Chat: POST https://mpp.t2000.ai/openai/v1/chat/completions {"model":"gpt-4o","messages":[...]}
- News: POST https://mpp.t2000.ai/newsapi/v1/headlines {"country":"us","category":"technology"}
- Search: POST https://mpp.t2000.ai/brave/v1/web/search {"q":"query"}
- Image: POST https://mpp.t2000.ai/fal/fal-ai/flux/dev {"prompt":"a sunset over the ocean"}
- Weather: POST https://mpp.t2000.ai/openweather/v1/weather {"q":"Tokyo"}
- Translate: POST https://mpp.t2000.ai/deepl/v1/translate {"text":["Hello"],"target_lang":"ES"}
- Email: POST https://mpp.t2000.ai/resend/v1/emails {"from":"...","to":"...","subject":"...","text":"..."}
- Crypto prices: POST https://mpp.t2000.ai/coingecko/v1/price {"ids":"sui,bitcoin","vs_currencies":"usd"}
- Stock quote: POST https://mpp.t2000.ai/alphavantage/v1/quote {"symbol":"AAPL"}
- Code exec: POST https://mpp.t2000.ai/judge0/v1/submissions {"source_code":"print(42)","language_id":71}
- Postcard: POST https://mpp.t2000.ai/lob/v1/postcards {"to":{...},"from":{...},"front":"...","back":"..."}
- Flights: POST https://mpp.t2000.ai/serpapi/v1/flights {"departure_id":"LAX","arrival_id":"NRT","outbound_date":"2026-05-01","type":"2"}
- URL shorten: POST https://mpp.t2000.ai/shortio/v1/shorten {"url":"https://example.com"}
- Security scan: POST https://mpp.t2000.ai/virustotal/v1/scan {"url":"https://suspicious-site.com"}
- Forex: POST https://mpp.t2000.ai/exchangerate/v1/convert {"from":"USD","to":"EUR","amount":100}
- Push notification: POST https://mpp.t2000.ai/pushover/v1/push {"user":"USER_KEY","message":"Alert!"}
- Mistral: POST https://mpp.t2000.ai/mistral/v1/chat/completions {"model":"mistral-large-latest","messages":[{"role":"user","content":"Hello"}]}
- Cohere: POST https://mpp.t2000.ai/cohere/v1/chat {"model":"command-r-plus","message":"Hello"}

`,
    {
      url: z.string().describe('Full URL of the MPP service endpoint (use t2000_services to discover available URLs)'),
      method: z.enum(['GET', 'POST', 'PUT', 'DELETE']).default('POST').describe('HTTP method (most services use POST)'),
      body: z.string().optional().describe('JSON request body (required for POST endpoints)'),
      headers: z.record(z.string()).optional().describe('Additional HTTP headers'),
      maxPrice: z.number().default(1.0).describe('Max USD to pay (default: $1.00). Set higher for commerce services.'),
    },
    async ({ url, method, body, headers, maxPrice }) => {
      try {
        const result = await mutex.run(() =>
          agent.pay({ url, method, body, headers, maxPrice }),
        );

        let text = JSON.stringify(result);

        // Extract image URLs and prepend them for visibility
        try {
          const data = typeof result === 'string' ? JSON.parse(result) : result;
          const imageUrls = extractImageUrls(data);
          if (imageUrls.length > 0) {
            const urlList = imageUrls.slice(0, 4).map((u) => `- ${u}`).join('\n');
            text = `Generated images:\n${urlList}\n\n${text}`;
          }
        } catch { /* not JSON or no images */ }

        // Cap response at 800KB to stay under Claude Desktop's 1MB tool result limit
        const MAX_BYTES = 800_000;
        if (text.length > MAX_BYTES) {
          text = text.slice(0, MAX_BYTES) + '\n\n[Response truncated — exceeded size limit]';
        }

        return { content: [{ type: 'text' as const, text }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  // ---------------------------------------------------------------------------
  // Contact management
  // ---------------------------------------------------------------------------

  // ---------------------------------------------------------------------------
  // Swap (Cetus Aggregator)
  // ---------------------------------------------------------------------------

  server.tool(
    't2000_swap',
    'Swap tokens on Sui via Cetus Aggregator (20+ DEXs). Supports any token pair with liquidity. Use user-friendly names (SUI, USDC, CETUS, DEEP, etc.) or full coin types.',
    {
      from: z.string().describe('Source token (e.g. "SUI", "USDC", or full coin type)'),
      to: z.string().describe('Target token (e.g. "USDC", "CETUS", or full coin type)'),
      amount: z.number().positive().describe('Amount to swap'),
      slippage: z.number().min(0.001).max(0.05).optional().describe('Max slippage (default 0.01 = 1%, max 5%)'),
    },
    async ({ from, to, amount, slippage }) => {
      try {
        const result = await mutex.run(() => agent.swap({ from, to, amount, slippage }));
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  // ---------------------------------------------------------------------------
  // VOLO vSUI Liquid Staking
  // ---------------------------------------------------------------------------

  server.tool(
    't2000_stake',
    'Stake SUI for vSUI via VOLO liquid staking. Earn ~3-5% APY. Rewards compound automatically via exchange rate. Minimum 1 SUI.',
    {
      amount: z.number().min(1).describe('Amount of SUI to stake (minimum 1)'),
    },
    async ({ amount }) => {
      try {
        const result = await mutex.run(() => agent.stakeVSui({ amount }));
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  server.tool(
    't2000_unstake',
    'Unstake vSUI back to SUI. Returns SUI including accumulated yield. Use amount in vSUI units or "all" to unstake entire position.',
    {
      amount: z.union([z.number().positive(), z.literal('all')]).describe('Amount of vSUI to unstake, or "all"'),
    },
    async ({ amount }) => {
      try {
        const result = await mutex.run(() => agent.unstakeVSui({ amount }));
        return { content: [{ type: 'text', text: JSON.stringify(result) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  // ---------------------------------------------------------------------------
  // Contact management
  // ---------------------------------------------------------------------------

  server.tool(
    't2000_contact_add',
    'Save a contact name → Sui address mapping. After saving, use the name with t2000_send instead of pasting addresses. Example: save "Tom" as 0x1234... then send to "Tom".',
    {
      name: z.string().describe('Contact name (e.g. "Tom", "Alice")'),
      address: z.string().describe('Sui wallet address (0x...)'),
    },
    async ({ name, address }) => {
      try {
        const result = agent.contacts.add(name, address);
        return { content: [{ type: 'text', text: JSON.stringify({ success: true, name, address, ...result }) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );

  server.tool(
    't2000_contact_remove',
    'Remove a saved contact by name.',
    {
      name: z.string().describe('Contact name to remove'),
    },
    async ({ name }) => {
      try {
        const removed = agent.contacts.remove(name);
        return { content: [{ type: 'text', text: JSON.stringify({ success: removed, name }) }] };
      } catch (err) {
        return errorResult(err);
      }
    },
  );
}
