import type { SerializedType, SerializedValue } from './serialize-provable.ts';
import type { Json } from './types.ts';
import type {
  ConstantInputJSON,
  CredentialSpecJSON,
  InputJSON,
  NodeJSON,
  PresentationRequestJSON,
  StoredCredentialJSON,
} from './validation.ts';

export { PrettyPrinter };

/**
 * Methods to print Mina Attestation data types
 * in human readable format.
 */
const PrettyPrinter = {
  printPresentationRequest,
  printVerifierIdentity,
  simplifyCredentialData,
};

function printPresentationRequest(request: PresentationRequestJSON): string {
  let formatted = [
    `Type: ${request.type}`,
    '',
    formatInputsHumanReadable(request.spec.inputs),
    '',
    `Requirements:\n${formatLogicNode(request.spec.assert, 0)}`,
    '',
    `Output:\n${formatLogicNode(request.spec.outputClaim, 0)}`,
    formatClaimsHumanReadable(request.claims),
    request.inputContext
      ? `\nContext:\n- Type: ${request.inputContext.type}\n- Action: ${request.inputContext.action}\n- Server Nonce: ${request.inputContext.serverNonce.value}`
      : 'WARNING: This request is not bound to any context',
  ].join('\n');

  return formatted;
}

function printVerifierIdentity(
  request: PresentationRequestJSON,
  origin: string
): string {
  if (request.type === 'no-context') {
    return '\nWARNING: No verifier identity provided\n';
  }
  if (request.type === 'https') {
    return `\nVerifier Identity: ${origin}\n`;
  }

  // for zkapp requests, verifier identity is contained in the presentation request
  if (request.inputContext?.type !== 'zk-app') {
    return '\nWARNING: Invalid request!\n';
  }

  let { verifierIdentity } = request.inputContext;
  let verifierUrl = `minascan.io/${verifierIdentity.network}/account/${verifierIdentity.publicKey}`;

  return `\nVerifier Identity: ${JSON.stringify(verifierIdentity, null, 2)}

See verifying zkApp on Minascan: https://${verifierUrl}\n`;
}

function simplifyCredentialData(storedCredential: StoredCredentialJSON): Json {
  let data = getCredentialData(storedCredential.credential);
  if (typeof data !== 'object' || data === null || Array.isArray(data)) {
    return data;
  }
  let dataObject: Record<string, Json | SerializedValue> = data;
  let simplified: Record<string, Json> = {};
  for (let [key, value] of Object.entries(dataObject)) {
    if (typeof value === 'object' && value !== null && 'value' in value) {
      simplified[key] = value.value;
    } else {
      simplified[key] = value;
    }
  }
  return simplified;
}

function getCredentialData(
  credential: StoredCredentialJSON['credential']
):
  | Json
  | Record<
      string,
      string | number | boolean | (SerializedType & { value: Json })
    > {
  if ('value' in credential) {
    // TODO get rid of type coercions
    return credential.value.data as any;
  }
  return credential.data;
}

function extractCredentialFields(data: any): string[] {
  if (!data) return [];

  if (data._type === 'Struct' && data.properties) {
    return Object.keys(data.properties);
  }

  if (data._type === 'DynamicRecord' && data.knownShape) {
    return Object.keys(data.knownShape);
  }

  return Object.keys(data);
}

function buildPropertyPath(node: NodeJSON): string {
  let parts: string[] = [];
  let currentNode: NodeJSON | undefined = node;

  while (currentNode?.type === 'property') {
    parts.unshift(currentNode.key);
    currentNode = currentNode.inner;
  }

  return parts.join('.');
}

function formatLogicNode(node: NodeJSON, level = 0): string {
  let indent = '  '.repeat(level);

  switch (node.type) {
    case 'and':
      if (node.inputs.length === 0) {
        return 'true';
      }
      return `${indent}All of these conditions must be true:\n${node.inputs
        .map((n) => `${indent}- ${formatLogicNode(n, level + 1)}`)
        .join('\n')}`;

    case 'or':
      return `${indent}Either:\n${indent}- ${formatLogicNode(
        node.left,
        level + 1
      )}\n${indent}Or:\n${indent}- ${formatLogicNode(node.right, level + 1)}`;

    case 'equals':
      return `${formatLogicNode(node.left)} = ${formatLogicNode(node.right)}`;

    case 'equalsOneOf': {
      let input = formatLogicNode(node.input, level);
      let options = Array.isArray(node.options)
        ? node.options.map((o) => formatLogicNode(o, level)).join(', ')
        : formatLogicNode(node.options, level);
      return `${options} contains ${input}`;
    }

    case 'lessThan':
      return `${formatLogicNode(node.left)} < ${formatLogicNode(node.right)}`;

    case 'lessThanEq':
      return `${formatLogicNode(node.left)} ≤ ${formatLogicNode(node.right)}`;

    case 'property': {
      // If this is the root property, just return the path
      if (node.inner?.type === 'root') {
        return node.key;
      }
      // For nested properties, build the complete path
      return buildPropertyPath(node);
    }

    case 'root':
      return '';

    case 'hash':
      return `hash(${node.inputs
        .map((n) => formatLogicNode(n, level))
        .join(', ')})`;

    case 'issuer':
      return `issuer(${node.credentialKey})`;
    case 'not':
      if (node.inner.type === 'equals') {
        return `${formatLogicNode(node.inner.left)} ≠ ${formatLogicNode(
          node.inner.right
        )}`;
      }
      return `not(${formatLogicNode(node.inner, level)})`;
    case 'add':
      return `(${formatLogicNode(node.left)} + ${formatLogicNode(node.right)})`;
    case 'sub':
      return `(${formatLogicNode(node.left)} - ${formatLogicNode(node.right)})`;
    case 'mul':
      return `(${formatLogicNode(node.left)} x ${formatLogicNode(node.right)})`;

    case 'div':
      return `(${formatLogicNode(node.left)} ÷ ${formatLogicNode(node.right)})`;

    case 'record': {
      if (Object.keys(node.data).length === 0) {
        return '{}';
      }
      return Object.entries(node.data)
        .map(([key, value]) => `${key}: ${formatLogicNode(value, level)}`)
        .join(`\n${indent}`);
    }
    case 'constant': {
      if (node.data._type === 'Undefined') return 'undefined';
      return formatJson(node.data.value);
    }
    case 'ifThenElse':
      return `${indent}If this condition is true:\n${indent}- ${formatLogicNode(
        node.condition,
        level + 1
      )}\n${indent}Then:\n${indent}- ${formatLogicNode(
        node.thenNode,
        level + 1
      )}\n${indent}Otherwise:\n${indent}- ${formatLogicNode(
        node.elseNode,
        level + 1
      )}`;
    case 'credential': {
      return node.credentialKey;
    }
    case 'owner': {
      return 'OWNER';
    }
    case 'issuerPublicKey': {
      return `issuerPublicKey(${node.credentialKey})`;
    }
    case 'publicInput': {
      return `publicInput(${node.credentialKey})`;
    }
    case 'verificationKeyHash': {
      return `verificationKeyHash(${node.credentialKey})`;
    }
    default:
      return `<UNKNOWN OPERATION '${(node satisfies never as any).type}'>`;
  }
}

// TODO here we assume that it makes sense to convert general serialized provable values to strings
// but they can be objects etc. doesn't work for Int64 for example

function formatInputsHumanReadable(inputs: Record<string, InputJSON>): string {
  let sections: string[] = [];

  // Handle credentials
  let credentials = Object.entries(inputs).filter(
    (input): input is [string, CredentialSpecJSON] =>
      input[1].type === 'credential'
  );
  if (credentials.length > 0) {
    sections.push('Required credentials:');
    for (let [key, input] of credentials) {
      let fields = extractCredentialFields(input.data);
      let wrappedFields = fields.reduce((acc, field, i) => {
        if (i === fields.length - 1) return acc + field;
        return `${acc + field}, `;
      }, '');
      sections.push(
        `- ${key} (type: ${input.credentialType}):\n  Contains: ${wrappedFields}`
      );
    }
  }

  // Handle claims
  let claims = Object.entries(inputs).filter(
    ([_, input]) => input.type === 'claim'
  );
  if (claims.length > 0) {
    sections.push('\nClaims:');
    for (let [key, input] of claims) {
      sections.push(`- ${key}: ${input.data._type}`);
    }
  }

  // Handle constants
  let constants = Object.entries(inputs).filter(
    (input): input is [string, ConstantInputJSON] =>
      input[1].type === 'constant'
  );
  if (constants.length > 0) {
    sections.push('\nConstants:');
    for (let [key, input] of constants) {
      sections.push(
        `- ${key}: ${input.data._type} = ${formatJson(input.value)}`
      );
    }
  }

  return sections.join('\n');
}

function formatClaimsHumanReadable(
  claims: Record<string, SerializedValue>
): string {
  let sections = ['\nClaimed values:'];

  for (let [key, claim] of Object.entries(claims)) {
    sections.push(`- ${key}: ${formatJson(claim.value)}`);
  }
  return sections.join('\n');
}

function formatJson(value: Json) {
  if (typeof value === 'string') return value;
  return JSON.stringify(value);
}
