import { getExplorerApiBase, fetchBlockscoutJson } from './blockscout'
import { keccak_256 } from 'js-sha3'

export interface ContractMethodParam {
  name: string
  type: string
}

export interface ContractMethod {
  name: string
  signature: string
  stateMutability: string
  inputs: ContractMethodParam[]
  outputs: ContractMethodParam[]
}

export interface ContractMethodExecutionResult {
  value: string
}

export interface ContractSourceFile {
  path: string
  content: string
}

export interface ContractProfile {
  has_custom_methods_read: boolean
  has_custom_methods_write: boolean
  proxy_type?: string
  is_self_destructed?: boolean
  implementations: string[]
  creation_bytecode?: string
  deployed_bytecode?: string
  source_verified: boolean
  abi_available: boolean
  contract_name?: string
  compiler_version?: string
  optimization_enabled?: boolean
  optimization_runs?: number
  evm_version?: string
  license_type?: string
  constructor_arguments?: string
  abi?: string
  abi_full?: string
  source_code_full?: string
  source_code_preview?: string
  source_files: ContractSourceFile[]
  source_status_text?: string
  read_methods: ContractMethod[]
  write_methods: ContractMethod[]
}

function truncateHex(value?: string | null, maxLength = 66): string | undefined {
  if (!value) return undefined
  if (value.length <= maxLength) return value
  return `${value.slice(0, maxLength)}...`
}

function truncateText(value?: string | null, maxLength = 400): string | undefined {
  if (!value) return undefined
  if (value.length <= maxLength) return value
  return `${value.slice(0, maxLength)}...`
}

interface ContractCompatibilityAbiResponse {
  status?: string | null
  message?: string | null
  result?: string | null
}

interface ContractCompatibilitySourceRecord {
  Address?: string
  ContractName?: string
  FileName?: string
  CompilerVersion?: string
  OptimizationUsed?: string | number
  Runs?: string | number
  EVMVersion?: string
  LicenseType?: string
  ConstructorArguments?: string
  SourceCode?: string
  ABI?: string
}

interface ContractCompatibilitySourceResponse {
  status?: string | null
  message?: string | null
  result?: ContractCompatibilitySourceRecord[] | null
}

interface ABIEntry {
  type?: string
  name?: string
  stateMutability?: string
  constant?: boolean
  inputs?: Array<{ name?: string; type?: string }>
  outputs?: Array<{ name?: string; type?: string }>
}

async function fetchCompatJson<T>(params: URLSearchParams): Promise<T> {
  const base = typeof window !== 'undefined' ? '' : getExplorerApiBase()
  const response = await fetch(`${base}/api/?${params.toString()}`)
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }
  return response.json() as Promise<T>
}

function normalizeBooleanFlag(value: string | number | null | undefined): boolean | undefined {
  if (value == null || value === '') return undefined
  if (typeof value === 'number') return value === 1
  return value === '1' || value.toLowerCase() === 'true'
}

function normalizeNumber(value: string | number | null | undefined): number | undefined {
  if (typeof value === 'number' && Number.isFinite(value)) return value
  if (typeof value === 'string' && value.trim() !== '') {
    const parsed = Number(value)
    if (Number.isFinite(parsed)) return parsed
  }
  return undefined
}

function displaySourcePath(record: ContractCompatibilitySourceRecord | undefined): string {
  const fileName = record?.FileName?.trim()
  if (fileName) return fileName
  const contractName = record?.ContractName?.trim()
  if (contractName) return contractName.endsWith('.sol') ? contractName : `${contractName}.sol`
  return 'Contract.sol'
}

function parseSourceFiles(sourceCode: string | undefined, record?: ContractCompatibilitySourceRecord): ContractSourceFile[] {
  const trimmed = sourceCode?.trim()
  if (!trimmed) return []

  const candidates = [trimmed]
  if (trimmed.startsWith('{{') && trimmed.endsWith('}}')) {
    candidates.push(trimmed.slice(1, -1))
  }

  for (const candidate of candidates) {
    try {
      const parsed = JSON.parse(candidate) as {
        sources?: Record<string, { content?: string } | string>
      }
      if (parsed && typeof parsed === 'object' && parsed.sources && typeof parsed.sources === 'object') {
        return Object.entries(parsed.sources)
          .map(([path, value]) => ({
            path,
            content: typeof value === 'string' ? value : value?.content || '',
          }))
          .filter((file) => file.content.trim().length > 0)
      }
    } catch {}
  }

  return [
    {
      path: displaySourcePath(record),
      content: trimmed,
    },
  ]
}

function parseABI(abiString?: string): ContractMethod[] {
  if (!abiString) return []
  try {
    const parsed = JSON.parse(abiString) as ABIEntry[]
    if (!Array.isArray(parsed)) return []
    return parsed
      .filter((entry) => entry.type === 'function' && entry.name)
      .map((entry) => {
        const inputs = Array.isArray(entry.inputs)
          ? entry.inputs.map((input) => ({
              name: input.name || '',
              type: input.type || 'unknown',
            }))
          : []
        const outputs = Array.isArray(entry.outputs)
          ? entry.outputs.map((output) => ({
              name: output.name || '',
              type: output.type || 'unknown',
            }))
          : []
        return {
          name: entry.name || 'unknown',
          signature: `${entry.name || 'unknown'}(${inputs.map((input) => input.type).join(',')})`,
          stateMutability:
            entry.stateMutability ||
            (entry.constant || outputs.length > 0 ? 'view' : 'nonpayable'),
          inputs,
          outputs,
        }
      })
  } catch {
    return []
  }
}

function isReadMethod(method: ContractMethod): boolean {
  return method.stateMutability === 'view' || method.stateMutability === 'pure'
}

function isSupportedInputType(type: string): boolean {
  return ['address', 'bool', 'string', 'uint256', 'uint8', 'bytes32'].includes(type)
}

function isSupportedOutputType(type: string): boolean {
  return ['address', 'bool', 'string', 'uint256', 'uint8', 'bytes32', 'bytes'].includes(type)
}

function supportsSimpleReadCall(method: ContractMethod): boolean {
  return (
    method.outputs.length === 1 &&
    method.inputs.every((input) => isSupportedInputType(input.type)) &&
    method.outputs.every((output) => isSupportedOutputType(output.type))
  )
}

function supportsSimpleWriteCall(method: ContractMethod): boolean {
  return !isReadMethod(method) && method.inputs.every((input) => isSupportedInputType(input.type))
}

function getPublicRpcUrl(): string {
  return process.env.NEXT_PUBLIC_RPC_URL_138 || 'https://rpc-http-pub.d-bis.org'
}

function decodeDynamicString(wordData: string, offset: number): string {
  const lengthHex = wordData.slice(offset, offset + 64)
  const length = parseInt(lengthHex || '0', 16)
  const start = offset + 64
  const end = start + length * 2
  const contentHex = wordData.slice(start, end)
  if (!contentHex) return ''
  const bytes = contentHex.match(/.{1,2}/g) || []
  return bytes
    .map((byte) => String.fromCharCode(parseInt(byte, 16)))
    .join('')
    .replace(/\u0000+$/g, '')
}

function validateAndEncodeInput(type: string, value: string): { head: string; tail?: string } {
  const trimmed = value.trim()
  switch (type) {
    case 'address': {
      if (!/^0x[a-fA-F0-9]{40}$/.test(trimmed)) {
        throw new Error('Address inputs must be full 0x-prefixed addresses.')
      }
      return { head: trimmed.slice(2).toLowerCase().padStart(64, '0') }
    }
    case 'bool':
      if (!['true', 'false', '1', '0'].includes(trimmed.toLowerCase())) {
        throw new Error('Boolean inputs must be true/false or 1/0.')
      }
      return { head: (trimmed === 'true' || trimmed === '1' ? '1' : '0').padStart(64, '0') }
    case 'uint256':
    case 'uint8': {
      if (!/^\d+$/.test(trimmed)) {
        throw new Error('Unsigned integer inputs must be non-negative decimal numbers.')
      }
      return { head: BigInt(trimmed).toString(16).padStart(64, '0') }
    }
    case 'bytes32': {
      if (!/^0x[a-fA-F0-9]{64}$/.test(trimmed)) {
        throw new Error('bytes32 inputs must be 32-byte 0x-prefixed hex values.')
      }
      return { head: trimmed.slice(2).toLowerCase() }
    }
    case 'string': {
      const contentHex = Array.from(new TextEncoder().encode(trimmed))
        .map((byte) => byte.toString(16).padStart(2, '0'))
        .join('')
      const paddedContent = contentHex.padEnd(Math.ceil(contentHex.length / 64) * 64 || 64, '0')
      const lengthHex = contentHex.length / 2
      return {
        head: '',
        tail:
          BigInt(lengthHex).toString(16).padStart(64, '0') +
          paddedContent,
      }
    }
    default:
      throw new Error(`Unsupported input type ${type}`)
  }
}

export function encodeMethodCalldata(method: ContractMethod, values: string[]): string {
  if (values.length !== method.inputs.length) {
    throw new Error('Method input count does not match the provided values.')
  }

  const selector = keccak_256(method.signature).slice(0, 8)
  const encodedInputs = method.inputs.map((input, index) => validateAndEncodeInput(input.type, values[index] || ''))
  let dynamicOffsetWords = method.inputs.length * 32
  const heads = encodedInputs.map((encoded) => {
    if (encoded.tail != null) {
      const head = BigInt(dynamicOffsetWords).toString(16).padStart(64, '0')
      dynamicOffsetWords += encoded.tail.length / 2
      return head
    }
    return encoded.head
  })
  const tails = encodedInputs
    .filter((encoded) => encoded.tail != null)
    .map((encoded) => encoded.tail || '')
    .join('')

  return `0x${selector}${heads.join('')}${tails}`
}

function decodeSimpleOutput(outputType: string, data: string): string {
  const normalized = data.replace(/^0x/i, '')
  if (!normalized) return 'No data returned'

  switch (outputType) {
    case 'address':
      return `0x${normalized.slice(24, 64)}`
    case 'bool':
      return BigInt(`0x${normalized.slice(0, 64)}`) === 0n ? 'false' : 'true'
    case 'string': {
      const offset = Number(BigInt(`0x${normalized.slice(0, 64)}`) * 2n)
      return decodeDynamicString(normalized, offset)
    }
    case 'bytes32':
      return `0x${normalized.slice(0, 64)}`
    case 'bytes': {
      const offset = Number(BigInt(`0x${normalized.slice(0, 64)}`) * 2n)
      const length = parseInt(normalized.slice(offset + 64, offset + 128) || '0', 16)
      const start = offset + 128
      return `0x${normalized.slice(start, start + length * 2)}`
    }
    default:
      if (outputType.startsWith('uint') || outputType.startsWith('int')) {
        return BigInt(`0x${normalized.slice(0, 64)}`).toString()
      }
      return `0x${normalized}`
  }
}

export async function callSimpleReadMethod(address: string, method: ContractMethod, values: string[] = []): Promise<string> {
  if (!supportsSimpleReadCall(method)) {
    throw new Error('Only simple read methods with supported input and output types are supported in this explorer surface.')
  }
  const data = encodeMethodCalldata(method, values)
  const response = await fetch(getPublicRpcUrl(), {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'eth_call',
      params: [
        {
          to: address,
          data,
        },
        'latest',
      ],
    }),
  })

  if (!response.ok) {
    throw new Error(`RPC HTTP ${response.status}`)
  }

  const payload = (await response.json()) as { result?: string; error?: { message?: string } }
  if (payload.error?.message) {
    throw new Error(payload.error.message)
  }
  const result = payload.result || '0x'
  return decodeSimpleOutput(method.outputs[0]?.type || 'bytes', result)
}

export const contractsApi = {
  getProfileSafe: async (address: string): Promise<{ ok: boolean; data: ContractProfile | null }> => {
    try {
      const [raw, abiResponse, sourceResponse] = await Promise.all([
        fetchBlockscoutJson<{
          has_custom_methods_read?: boolean
          has_custom_methods_write?: boolean
          proxy_type?: string | null
          is_self_destructed?: boolean | null
          implementations?: Array<{ address?: string | null } | string>
          creation_bytecode?: string | null
          deployed_bytecode?: string | null
        }>(`/api/v2/smart-contracts/${address}`),
        fetchCompatJson<ContractCompatibilityAbiResponse>(
          new URLSearchParams({
            module: 'contract',
            action: 'getabi',
            address,
          }),
        ).catch(() => null),
        fetchCompatJson<ContractCompatibilitySourceResponse>(
          new URLSearchParams({
            module: 'contract',
            action: 'getsourcecode',
            address,
          }),
        ).catch(() => null),
      ])

      const sourceRecord = Array.isArray(sourceResponse?.result) ? sourceResponse?.result[0] : undefined
      const abiString =
        abiResponse?.status === '1' && abiResponse.result && abiResponse.result !== 'Contract source code not verified'
          ? abiResponse.result
          : sourceRecord?.ABI && sourceRecord.ABI !== 'Contract source code not verified'
            ? sourceRecord.ABI
            : undefined
      const sourceCode = sourceRecord?.SourceCode
      const sourceFiles = parseSourceFiles(sourceCode, sourceRecord)
      const parsedMethods = parseABI(abiString)
      const sourceVerified = Boolean(
        abiString ||
          (sourceCode && sourceCode.trim().length > 0) ||
          (sourceRecord?.ContractName && sourceRecord.ContractName.trim().length > 0),
      )
      const sourceStatusText = abiResponse?.message || sourceResponse?.message || (sourceVerified ? 'Verified source available' : 'Contract source code not verified')

      return {
        ok: true,
        data: {
          has_custom_methods_read: !!raw.has_custom_methods_read,
          has_custom_methods_write: !!raw.has_custom_methods_write,
          proxy_type: raw.proxy_type || undefined,
          is_self_destructed: raw.is_self_destructed ?? undefined,
          implementations: Array.isArray(raw.implementations)
            ? raw.implementations
                .map((entry) => typeof entry === 'string' ? entry : entry.address || '')
                .filter(Boolean)
            : [],
          creation_bytecode: truncateHex(raw.creation_bytecode),
          deployed_bytecode: truncateHex(raw.deployed_bytecode),
          source_verified: sourceVerified,
          abi_available: Boolean(abiString),
          contract_name: sourceRecord?.ContractName || undefined,
          compiler_version: sourceRecord?.CompilerVersion || undefined,
          optimization_enabled: normalizeBooleanFlag(sourceRecord?.OptimizationUsed),
          optimization_runs: normalizeNumber(sourceRecord?.Runs),
          evm_version: sourceRecord?.EVMVersion || undefined,
          license_type: sourceRecord?.LicenseType || undefined,
          constructor_arguments: truncateHex(sourceRecord?.ConstructorArguments, 90),
          abi: truncateText(abiString, 1200),
          abi_full: abiString,
          source_code_full: sourceCode,
          source_code_preview: truncateText(sourceCode, 1200),
          source_files: sourceFiles,
          source_status_text: sourceStatusText || undefined,
          read_methods: parsedMethods.filter(isReadMethod),
          write_methods: parsedMethods.filter((method) => !isReadMethod(method)),
        },
      }
    } catch {
      return { ok: false, data: null }
    }
  },
  supportsSimpleReadCall,
  supportsSimpleWriteCall,
}
