import {decodeEventLog,decodeFunctionData,encodeAbiParameters,keccak256,parseAbi,parseAbiParameters,toBytes,type Hex} from 'viem';
import {managerAbi} from './abi';
import type {Attribution} from './attribution';
import type {CallTrace} from './trace-attribution';

const transferAbi=parseAbi(['event Transfer(address indexed from,address indexed to,uint256 value)']);
const swapAbi=parseAbi(['function swap((address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks) key,(bool zeroForOne,int256 amountSpecified,uint160 sqrtPriceLimitX96) params,bytes hookData) returns(int256)']);
const otherSettlement=new Set([
 keccak256(toBytes('ModifyLiquidity(bytes32,address,int24,int24,int256,bytes32)')),
 keccak256(toBytes('Donate(bytes32,address,uint256,uint256)')),
]);
export type AssetInput={manager:string;target:string;poolId:string;targetDelta:bigint;currency0:string;from:string;to:string|null;input:Hex;value?:bigint;trace?:CallTrace|null;logs:{address:string;data:Hex;topics:readonly Hex[]}[]};

// Attribution means the target token's asset endpoint, not the human owner of
// a wallet, a router's msg.sender, or the beneficiary of another currency.
export function assetAttributeV2(t:AssetInput):Attribution {
 const manager=t.manager.toLowerCase(),target=t.target.toLowerCase();
 const evidence:Record<string,unknown>={parser:'target-asset-endpoint-v2',from:t.from,to:t.to,input:t.input,value:t.value?.toString()??null,policy:'sell: unique KING source; buy: unique final KING recipient; contract endpoint is attributed to that contract, never its inferred owner'};
 const reject=(reason:string):Attribution=>({user:null,recipient:null,status:'excluded',reason,evidence});
 try{
  const managerLogs=t.logs.filter(l=>l.address.toLowerCase()===manager);
  if(managerLogs.some(l=>otherSettlement.has(l.topics[0])))return reject('asset-flow: liquidity-or-donation-in-transaction');
  const swaps=managerLogs.flatMap(l=>{try{const d=decodeEventLog({abi:managerAbi,data:l.data,topics:l.topics as [Hex,...Hex[]]});return d.eventName==='Swap'?[d.args]:[];}catch{return[];}});
  const selected=swaps.filter(s=>s.id===t.poolId);
  if(selected.length!==1||(t.currency0.toLowerCase()===target?selected[0].amount0:selected[0].amount1)!==t.targetDelta)return reject('asset-flow: missing-or-multiple-target-swaps');
  const amount=t.targetDelta<0n?-t.targetDelta:t.targetDelta;
  if(amount===0n)return reject('zero-target-volume');
  const edges=t.logs.filter(l=>l.address.toLowerCase()===target).flatMap(l=>{try{const d=decodeEventLog({abi:transferAbi,data:l.data,topics:l.topics as [Hex,...Hex[]]});return d.args.value>0n?[{from:d.args.from.toLowerCase(),to:d.args.to.toLowerCase(),amount:d.args.value.toString()}]:[];}catch{return[];}});
  evidence.targetTransfers=edges;
  if(!edges.length||edges.some(e=>BigInt(e.amount)!==amount||e.from===e.to||[e.from,e.to].includes('0x'+'0'.repeat(40))))return reject('asset-flow: target-amount-or-mint-burn-mismatch');
  const buy=t.targetDelta>0n,visited=new Set<string>([manager]),used=new Set<number>();
  let endpoint=manager;
  // Follow the entire equal-amount linear path in receipt order. Branches,
  // cycles, fees in KING, retained balances, extra transfers fail closed.
  for(;;){
   const next=edges.map((e,i)=>({e,i})).filter(({e})=>buy?e.from===endpoint:e.to===endpoint);
   if(next.length===0)break;
   if(next.length!==1||used.has(next[0].i))return reject('asset-flow: ambiguous-target-path');
   const {e,i}=next[0],address=buy?e.to:e.from;
   if(visited.has(address))return reject('asset-flow: cyclic-target-path');
   used.add(i);visited.add(address);endpoint=address;
  }
  if(endpoint===manager||used.size!==edges.length)return reject('asset-flow: disconnected-or-ambiguous-target-path');
  const indices=[...used];
  if(indices.some((n,i)=>i>0&&(buy?n<=indices[i-1]:n>=indices[i-1])))return reject('asset-flow: transfer-order-mismatch');
  // Liquidity settlement or flash borrowing cannot be confused with the Swap:
  // the only target transfers must form this path to/from the singleton.
  if(swaps.length>1){
   const root=t.trace;
   if(!root||root.error||root.type!=='CALL'||root.from.toLowerCase()!==t.from.toLowerCase()||root.to?.toLowerCase()!==t.to?.toLowerCase()||root.input!==t.input||BigInt(root.value||'0x0')!==t.value)return reject('asset-flow: trace-required-for-multiple-pools');
   evidence.trace=root;
   const calls:CallTrace[]=[];
   function walk(c:CallTrace){if(c.error)return;if(c.type==='CALL'&&c.to?.toLowerCase()===manager&&c.input?.startsWith('0xf3cd914c'))calls.push(c);for(const child of c.calls||[])walk(child);}walk(root);
   if(calls.length!==swaps.length)return reject('asset-flow: trace-swap-count-mismatch');
   let targetCount=0;
   for(let i=0;i<calls.length;i++){
    const key=decodeFunctionData({abi:swapAbi,data:calls[i].input!}).args[0];
    const id=keccak256(encodeAbiParameters(parseAbiParameters('address,address,uint24,int24,address'),[key.currency0,key.currency1,key.fee,key.tickSpacing,key.hooks]));
    if(id!==swaps[i].id||calls[i].from.toLowerCase()!==swaps[i].sender.toLowerCase())return reject('asset-flow: trace-pool-or-order-mismatch');
    if([key.currency0.toLowerCase(),key.currency1.toLowerCase()].includes(target))targetCount++;
   }
   if(targetCount!==1)return reject('asset-flow: missing-or-multiple-target-swaps');
  }
  evidence.targetEndpoint=endpoint;evidence.direction=buy?'buy':'sell';
  evidence.targetSource=buy?manager:endpoint;evidence.targetRecipient=buy?endpoint:manager;
  evidence.identity='Unique target-token asset endpoint, not a claim about the controlling person or other-currency recipient';
  return{user:endpoint,recipient:buy?endpoint:null,status:'included',reason:buy?'计入 · KING 最终接收地址（资产流 v2）':'计入 · KING 来源地址（资产流 v2）',evidence};
 }catch(e){evidence.error=String(e);return reject('asset-flow: malformed-receipt-or-trace');}
}

// A unique asset endpoint can fund several same-direction fills. The graph
// proves ownership at address level, without decoding an opaque relay ABI.
export function assetAttribute(t:AssetInput):Attribution {
 const manager=t.manager.toLowerCase(),target=t.target.toLowerCase();
 const evidence:Record<string,unknown>={parser:'target-asset-endpoint-v4',from:t.from,to:t.to,input:t.input,value:t.value?.toString()??null};
 const reject=(reason:string):Attribution=>({user:null,recipient:null,status:'excluded',reason,evidence});
 try{
  const logs=t.logs.filter(l=>l.address.toLowerCase()===manager);
  if(logs.some(l=>otherSettlement.has(l.topics[0])))return reject('asset-flow: liquidity-or-donation-in-transaction');
  const swaps=logs.flatMap(l=>{try{const d=decodeEventLog({abi:managerAbi,data:l.data,topics:l.topics as [Hex,...Hex[]]});return d.eventName==='Swap'?[d.args]:[];}catch{return[];}});
  if(!swaps.some(s=>s.id===t.poolId&&(t.currency0.toLowerCase()===target?s.amount0:s.amount1)===t.targetDelta))return reject('asset-flow: missing-or-multiple-target-swaps');
  if(t.targetDelta===0n)return reject('zero-target-volume');
  const edges=t.logs.filter(l=>l.address.toLowerCase()===target).flatMap(l=>{try{const d=decodeEventLog({abi:transferAbi,data:l.data,topics:l.topics as [Hex,...Hex[]]});return d.args.value>0n?[{from:d.args.from.toLowerCase(),to:d.args.to.toLowerCase(),amount:d.args.value.toString()}]:[];}catch{return[];}});
  evidence.targetTransfers=edges;
  if(!edges.length)return reject('asset-flow: no-target-transfers');
  if(edges.some(e=>[e.from,e.to].includes('0x'+'0'.repeat(40))))return reject('asset-flow: target-mint-burn');
  const adjacency=new Map<string,Set<string>>(),balances=new Map<string,bigint>();
  for(const e of edges){const n=BigInt(e.amount);balances.set(e.from,(balances.get(e.from)||0n)-n);balances.set(e.to,(balances.get(e.to)||0n)+n);const next=adjacency.get(e.from)||new Set<string>();next.add(e.to);adjacency.set(e.from,next);}
  const colors=new Map<string,number>();
  function visit(address:string):boolean{if(colors.get(address)===1)return false;if(colors.get(address)===2)return true;colors.set(address,1);for(const next of adjacency.get(address)||[])if(!visit(next))return false;colors.set(address,2);return true;}
  if([...balances.keys()].some(a=>!visit(a)))return reject('asset-flow: cyclic-target-path');
  const sources=[...balances].filter(([,n])=>n<0n),sinks=[...balances].filter(([,n])=>n>0n);
  if(sources.length!==1||sinks.length!==1)return reject('asset-flow: ambiguous-target-path');
  const [source,negative]=sources[0],[sink,gross]=sinks[0],buy=t.targetDelta>0n;
  if((buy?source:sink)!==manager||-negative!==gross)return reject('asset-flow: disconnected-or-ambiguous-target-path');
  const running=new Map<string,bigint>();
  for(const e of edges){const n=BigInt(e.amount);if(e.from!==source&&(running.get(e.from)||0n)<n)return reject('asset-flow: transfer-order-mismatch');running.set(e.from,(running.get(e.from)||0n)-n);running.set(e.to,(running.get(e.to)||0n)+n);}
  let poolDelta=t.targetDelta;
  if(swaps.length>1){
   const root=t.trace;
   if(!root||root.error||root.type!=='CALL'||root.from.toLowerCase()!==t.from.toLowerCase()||root.to?.toLowerCase()!==t.to?.toLowerCase()||root.input!==t.input||BigInt(root.value||'0x0')!==t.value)return reject('asset-flow: trace-required-for-multiple-pools');
   evidence.trace=root;const calls:CallTrace[]=[];
   function walk(c:CallTrace){if(c.error)return;if(c.type==='CALL'&&c.to?.toLowerCase()===manager&&c.input?.startsWith('0xf3cd914c'))calls.push(c);for(const child of c.calls||[])walk(child);}walk(root);
   if(calls.length!==swaps.length)return reject('asset-flow: trace-swap-count-mismatch');
   poolDelta=0n;const targetSwaps=[];
   for(let i=0;i<calls.length;i++){
    const key=decodeFunctionData({abi:swapAbi,data:calls[i].input!}).args[0];
    const id=keccak256(encodeAbiParameters(parseAbiParameters('address,address,uint24,int24,address'),[key.currency0,key.currency1,key.fee,key.tickSpacing,key.hooks]));
    if(id!==swaps[i].id||calls[i].from.toLowerCase()!==swaps[i].sender.toLowerCase())return reject('asset-flow: trace-pool-or-order-mismatch');
    if([key.currency0.toLowerCase(),key.currency1.toLowerCase()].includes(target)){
     const delta=key.currency0.toLowerCase()===target?swaps[i].amount0:swaps[i].amount1;
     if(delta===0n||(delta>0n)!==buy)return reject('asset-flow: mixed-target-directions');
     poolDelta+=delta;targetSwaps.push({poolId:id,delta:delta.toString()});
    }
   }
   evidence.targetSwaps=targetSwaps;
  }
  if(gross!==(poolDelta<0n?-poolDelta:poolDelta))return reject('asset-flow: target-amount-mismatch');
  const endpoint=buy?sink:source;evidence.direction=buy?'buy':'sell';evidence.targetSource=source;evidence.targetRecipient=sink;evidence.targetEndpoint=endpoint;evidence.grossTargetAmount=gross.toString();
  evidence.identity='Unique conserved target asset endpoint; same-direction fills may split or merge; no inference about controlling person or relay brand';
  return{user:endpoint,recipient:buy?endpoint:null,status:'included',reason:buy?'计入 · KING 最终接收地址（资产流 v4）':'计入 · KING 来源地址（资产流 v4）',evidence};
 }catch(e){evidence.error=String(e);return reject('asset-flow: malformed-receipt-or-trace');}
}
