const fs = require('fs');
const path = require('path');
const ts = require('typescript');

function escapeXml(value) {
  return `${value ?? ''}`
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

function createNode(tag, attrs = {}, children = []) {
  return { tag, attrs, children };
}

function renderNode(node) {
  const attrsText = Object.entries(node.attrs || {})
    .filter(([, value]) => value !== undefined && value !== null && value !== '')
    .map(([key, value]) => ` ${key}="${escapeXml(value)}"`)
    .join('');
  const children = node.children || [];
  if (!children.length) {
    return `<${node.tag}${attrsText}/>`;
  }
  return `<${node.tag}${attrsText}>${children.map(renderNode).join('')}</${node.tag}>`;
}

function parseTsDeclare(tsContent) {
  const sourceFile = ts.createSourceFile(
    'declare.ts',
    tsContent,
    ts.ScriptTarget.Latest,
    true,
    ts.ScriptKind.TS
  );
  const ctx = {
    topName: '',
    topAliases: new Map(),
    parameters: { interfaces: new Map(), aliases: new Map() },
    results: { interfaces: new Map(), aliases: new Map() }
  };
  function collectInSubNamespace(moduleDecl, bucket) {
    const body = moduleDecl.body;
    if (!body || !ts.isModuleBlock(body)) return;
    for (const stmt of body.statements) {
      if (ts.isInterfaceDeclaration(stmt)) {
        bucket.interfaces.set(stmt.name.text, stmt);
      } else if (ts.isTypeAliasDeclaration(stmt)) {
        bucket.aliases.set(stmt.name.text, stmt.type);
      }
    }
  }
  for (const stmt of sourceFile.statements) {
    if (!ts.isModuleDeclaration(stmt)) continue;
    if (!ts.isIdentifier(stmt.name)) continue;
    ctx.topName = stmt.name.text;
    const body = stmt.body;
    if (!body || !ts.isModuleBlock(body)) continue;
    for (const item of body.statements) {
      if (ts.isTypeAliasDeclaration(item)) {
        ctx.topAliases.set(item.name.text, item.type);
      } else if (ts.isModuleDeclaration(item) && ts.isIdentifier(item.name)) {
        if (item.name.text === '$parameters') {
          collectInSubNamespace(item, ctx.parameters);
        } else if (item.name.text === '$results') {
          collectInSubNamespace(item, ctx.results);
        }
      }
    }
    break;
  }
  return ctx;
}

function getRefName(typeNode) {
  if (!typeNode) return '';
  if (ts.isTypeReferenceNode(typeNode)) {
    const typeName = typeNode.typeName;
    if (ts.isIdentifier(typeName)) return typeName.text;
    if (ts.isQualifiedName(typeName)) {
      return `${typeName.left.getText()}.${typeName.right.getText()}`;
    }
    return typeName.getText();
  }
  return '';
}

function stripNullable(typeNode) {
  if (ts.isUnionTypeNode(typeNode)) {
    const filtered = typeNode.types.filter(
      t => t.kind !== ts.SyntaxKind.NullKeyword && t.kind !== ts.SyntaxKind.UndefinedKeyword
    );
    if (filtered.length === 1) return filtered[0];
  }
  return typeNode;
}

function mapSimpleDataType(typeNode) {
  const node = stripNullable(typeNode);
  if (!node) return 'string';
  if (ts.isUnionTypeNode(node)) {
    const members = node.types.map(stripNullable).filter(Boolean);
    const hasBoolean = members.some(
      t =>
        t.kind === ts.SyntaxKind.BooleanKeyword ||
        (ts.isLiteralTypeNode(t) &&
          (t.literal.kind === ts.SyntaxKind.TrueKeyword ||
            t.literal.kind === ts.SyntaxKind.FalseKeyword))
    );
    if (hasBoolean) return 'boolean';
    const hasNumber = members.some(
      t =>
        t.kind === ts.SyntaxKind.NumberKeyword ||
        (ts.isLiteralTypeNode(t) && ts.isNumericLiteral(t.literal))
    );
    if (hasNumber) return 'float';
    return 'string';
  }
  if (node.kind === ts.SyntaxKind.StringKeyword) return 'string';
  if (node.kind === ts.SyntaxKind.NumberKeyword) return 'float';
  if (node.kind === ts.SyntaxKind.BooleanKeyword) return 'boolean';
  if (node.kind === ts.SyntaxKind.AnyKeyword) return 'string';
  if (node.kind === ts.SyntaxKind.UnknownKeyword) return 'string';
  if (ts.isLiteralTypeNode(node)) return 'string';
  const ref = getRefName(node);
  if (ref === 'Date' || ref === 'dayjs.Dayjs' || ref === 'Dayjs') return 'date';
  return 'string';
}

function isSimpleTypeNode(typeNode) {
  const node = stripNullable(typeNode);
  if (!node) return false;
  if (ts.isUnionTypeNode(node)) {
    const members = node.types.map(stripNullable).filter(Boolean);
    if (!members.length) return false;
    return members.every(member => isSimpleTypeNode(member));
  }
  if (
    node.kind === ts.SyntaxKind.StringKeyword ||
    node.kind === ts.SyntaxKind.NumberKeyword ||
    node.kind === ts.SyntaxKind.BooleanKeyword ||
    node.kind === ts.SyntaxKind.AnyKeyword ||
    node.kind === ts.SyntaxKind.UnknownKeyword ||
    ts.isLiteralTypeNode(node)
  ) {
    return true;
  }
  if (ts.isTypeReferenceNode(node)) {
    const ref = getRefName(node);
    return ['string', 'number', 'boolean', 'Date', 'any', 'unknown', 'dayjs.Dayjs', 'Dayjs'].includes(ref);
  }
  return false;
}

function isFileType(typeNode) {
  const node = stripNullable(typeNode);
  if (!node) return false;
  if (ts.isTypeReferenceNode(node)) {
    const ref = getRefName(node);
    if (ref === 'File') return true;
    if (ref === 'Array' && node.typeArguments && node.typeArguments.length) {
      return isFileType(node.typeArguments[0]);
    }
  }
  if (ts.isArrayTypeNode(node)) {
    return isFileType(node.elementType);
  }
  return false;
}

function resolveTypeNode(typeNode, ctx, scope) {
  const node = stripNullable(typeNode);
  if (!node) return node;
  if (ts.isTypeReferenceNode(node)) {
    const typeName = node.typeName;
    if (ts.isQualifiedName(typeName)) {
      const left = typeName.left.getText();
      const right = typeName.right.getText();
      if (left === '$parameters') {
        if (ctx.parameters.aliases.has(right)) {
          return resolveTypeNode(ctx.parameters.aliases.get(right), ctx, 'parameters');
        }
        if (ctx.parameters.interfaces.has(right)) {
          return ctx.parameters.interfaces.get(right);
        }
      }
      if (left === '$results') {
        if (ctx.results.aliases.has(right)) {
          return resolveTypeNode(ctx.results.aliases.get(right), ctx, 'results');
        }
        if (ctx.results.interfaces.has(right)) {
          return ctx.results.interfaces.get(right);
        }
      }
    }
    if (ts.isIdentifier(typeName)) {
      const name = typeName.text;
      const bucket = scope === 'results' ? ctx.results : ctx.parameters;
      if (bucket.aliases.has(name)) {
        return resolveTypeNode(bucket.aliases.get(name), ctx, scope);
      }
      if (bucket.interfaces.has(name)) {
        return bucket.interfaces.get(name);
      }
    }
  }
  return node;
}

function getPropertiesFromInterfaceOrLiteral(node) {
  if (ts.isInterfaceDeclaration(node)) {
    return node.members.filter(m => ts.isPropertySignature(m));
  }
  if (ts.isTypeLiteralNode(node)) {
    return node.members.filter(m => ts.isPropertySignature(m));
  }
  return [];
}

function getDescriptionText(node) {
  const docs = node && node.jsDoc ? node.jsDoc : [];
  if (!docs || !docs.length) return '';
  const comments = docs
    .map(d => (typeof d.comment === 'string' ? d.comment : ''))
    .filter(Boolean);
  return comments.join(' ').trim();
}

function buildSimpleNode(name, required, typeNode, description = '') {
  return createNode('simple', {
    name,
    dataType: mapSimpleDataType(typeNode),
    required: required ? 'true' : 'false',
    description
  });
}

function buildModelNode(name, required, modelName, description = '') {
  return createNode('model', {
    name,
    model: modelName || 'Object',
    required: required ? 'true' : 'false',
    description
  });
}

function buildArrayNode(name, required, elementNode, description = '') {
  return createNode(
    'array',
    { name, required: required ? 'true' : 'false', description },
    elementNode ? [elementNode] : []
  );
}

function getTypeRefModelName(typeNode) {
  if (ts.isTypeReferenceNode(typeNode)) {
    const typeName = typeNode.typeName;
    if (ts.isIdentifier(typeName)) return typeName.text;
    if (ts.isQualifiedName(typeName)) return typeName.right.getText();
  }
  return '';
}

function buildNodeForType(typeNode, propName, required, ctx, scope) {
  const node = resolveTypeNode(typeNode, ctx, scope);
  const normalizedName = propName || 'data';
  if (!node) return null;
  if (isFileType(node)) {
    return createNode('file', { name: normalizedName, description: '' });
  }
  if (isSimpleTypeNode(node)) {
    return buildSimpleNode(normalizedName, required, node, '');
  }
  if (ts.isArrayTypeNode(node)) {
    const elementNode = buildNodeForType(
      node.elementType,
      '',
      true,
      ctx,
      scope
    );
    return buildArrayNode(normalizedName, required, elementNode, '');
  }
  if (ts.isTypeReferenceNode(node)) {
    const ref = getRefName(node);
    if (ref === 'Array' && node.typeArguments && node.typeArguments.length) {
      const elementNode = buildNodeForType(
        node.typeArguments[0],
        '',
        true,
        ctx,
        scope
      );
      return buildArrayNode(normalizedName, required, elementNode, '');
    }
    if (['string', 'number', 'boolean', 'Date', 'any', 'unknown'].includes(ref)) {
      return buildSimpleNode(normalizedName, required, node, '');
    }
    const resolved = resolveTypeNode(node, ctx, scope);
    if (ts.isInterfaceDeclaration(resolved) || ts.isTypeLiteralNode(resolved)) {
      return buildComplexNode(resolved, normalizedName, required, ctx, scope);
    }
    const modelName = getTypeRefModelName(node);
    return buildModelNode(normalizedName, required, modelName, '');
  }
  if (ts.isInterfaceDeclaration(node) || ts.isTypeLiteralNode(node)) {
    return buildComplexNode(node, normalizedName, required, ctx, scope);
  }
  return buildModelNode(normalizedName, required, 'Object', '');
}

function buildComplexNode(node, name, required, ctx, scope) {
  const members = getPropertiesFromInterfaceOrLiteral(node);
  const children = [];
  for (const member of members) {
    if (!member.type || !member.name) continue;
    const propName = member.name.getText().replace(/^['"]|['"]$/g, '');
    const propRequired = !member.questionToken;
    const child = buildNodeForType(member.type, propName, propRequired, ctx, scope);
    if (child && !child.attrs.description) {
      child.attrs.description = getDescriptionText(member);
    }
    if (child) children.push(child);
  }
  return createNode(
    'complex',
    { name: name || 'data', required: required ? 'true' : 'false', description: getDescriptionText(node) },
    children
  );
}

function buildMultipartIfNeeded(typeNode, ctx, scope) {
  const node = resolveTypeNode(typeNode, ctx, scope);
  if (!(ts.isInterfaceDeclaration(node) || ts.isTypeLiteralNode(node))) return null;
  const members = getPropertiesFromInterfaceOrLiteral(node);
  if (!members.length) return null;
  let hasFile = false;
  const children = [];
  for (const member of members) {
    if (!member.type || !member.name) continue;
    const propName = member.name.getText().replace(/^['"]|['"]$/g, '');
    if (isFileType(member.type)) {
      hasFile = true;
      children.push(createNode('file', { name: propName, description: getDescriptionText(member) }));
    } else {
      const simple = buildSimpleNode(propName, !member.questionToken, member.type, getDescriptionText(member));
      children.push(simple);
    }
  }
  if (!hasFile) return null;
  return createNode('multipart', {}, children);
}

function buildParametersNode(ctx) {
  const rootType = ctx.topAliases.get('Parameters');
  if (!rootType) return createNode('parameters');
  const resolved = resolveTypeNode(rootType, ctx, 'parameters');
  if (resolved && resolved.kind === ts.SyntaxKind.TypeLiteral && !resolved.members.length) {
    return createNode('parameters');
  }
  const multipart = buildMultipartIfNeeded(resolved, ctx, 'parameters');
  if (multipart) {
    return createNode('parameters', {}, [multipart]);
  }
  if (ts.isInterfaceDeclaration(resolved) || ts.isTypeLiteralNode(resolved)) {
    const members = getPropertiesFromInterfaceOrLiteral(resolved);
    if (!members.length) return createNode('parameters');
    const children = members.map(m => {
      const name = m.name.getText().replace(/^['"]|['"]$/g, '');
      const child = buildNodeForType(m.type, name, !m.questionToken, ctx, 'parameters');
      if (child && !child.attrs.description) {
        child.attrs.description = getDescriptionText(m);
      }
      return child;
    }).filter(Boolean);
    return createNode('parameters', {}, [
      createNode('complex', {
        name: 'jsonParam',
        required: 'false',
        description: getDescriptionText(resolved) || 'jsonParam'
      }, children)
    ]);
  }
  const node = buildNodeForType(resolved, 'parameters', true, ctx, 'parameters');
  if (!node) return createNode('parameters');
  return createNode('parameters', {}, [node]);
}

function buildResultNode(ctx) {
  const rootType = ctx.topAliases.get('Result');
  const attrs = { generic: '', contentType: '' };
  if (!rootType) return createNode('result', attrs);
  const resolved = resolveTypeNode(rootType, ctx, 'results');
  if (resolved && resolved.kind === ts.SyntaxKind.TypeLiteral && !resolved.members.length) {
    return createNode('result', attrs);
  }
  const node = buildNodeForType(resolved, 'result', true, ctx, 'results');
  if (!node) return createNode('result', attrs);
  return createNode('result', attrs, [node]);
}

function buildProtocolNode(protocolOptions = {}) {
  return createNode('protocol', {
    dispatchedBy: protocolOptions.dispatchedBy || 'head',
    method: protocolOptions.method || 'POST',
    enctype: protocolOptions.enctype || 'application/json'
  });
}

function convertTsDeclareContentToXml(tsContent, options = {}) {
  const ctx = parseTsDeclare(tsContent);
  const declareNode = createNode('declare', {}, [
    buildProtocolNode(options.protocol),
    buildParametersNode(ctx),
    buildResultNode(ctx)
  ]);
  return renderNode(declareNode);
}

function convertTsDeclareFileToXml(inputFilePath, options = {}) {
  const fullPath = path.resolve(inputFilePath);
  const content = fs.readFileSync(fullPath, 'utf8');
  const xml = convertTsDeclareContentToXml(content, options);
  if (options.outputFilePath) {
    fs.writeFileSync(path.resolve(options.outputFilePath), xml, 'utf8');
  }
  return xml;
}

if (require.main === module) {
  const inputFile = process.argv[2];
  const outputFile = process.argv[3];
  if (!inputFile) {
    console.error('请传入TS声明文件路径，例如: node TsDeclareToXml.js src/services/CarInDeclare.ts');
    process.exit(1);
  }
  const xml = convertTsDeclareFileToXml(inputFile, { outputFilePath: outputFile });
  if (!outputFile) {
    console.log(xml);
  }
}

module.exports = {
  convertTsDeclareContentToXml,
  convertTsDeclareFileToXml
};
