import * as fs from 'fs';
import * as path from 'path';
import Handlebars from 'handlebars';
import type { Result } from '@harness-engineering/core';
import { Ok, Err } from '@harness-engineering/core';
import { TemplateMetadataSchema, type TemplateMetadata } from './schema';
import { deepMergeJson, mergePackageJson } from './merger';

export interface TemplateContext {
  projectName: string;
  level?: string;
  framework?: string;
  language?: string;
  goModulePath?: string;
  pythonMinVersion?: string;
  javaGroupId?: string;
  rustEdition?: string;
}

interface TemplateFile {
  relativePath: string;
  absolutePath: string;
  isHandlebars: boolean;
  sourceTemplate: string;
}

export interface ResolvedTemplate {
  metadata: TemplateMetadata;
  files: TemplateFile[];
  overlayMetadata?: TemplateMetadata;
}

interface RenderedFile {
  relativePath: string;
  content: string;
}

export interface RenderedFiles {
  files: RenderedFile[];
}

export interface DetectedFramework {
  framework: string;
  language: string;
  score: number;
  templateName: string;
}

interface WriteOptions {
  overwrite: boolean;
  language?: string;
  existingProject?: boolean;
}

export interface WriteResult {
  written: string[];
  skippedConfigs: string[];
}

const NON_JSON_PACKAGE_CONFIGS = new Set([
  'pyproject.toml',
  'go.mod',
  'Cargo.toml',
  'pom.xml',
  'build.gradle',
  'build.gradle.kts',
]);

/** Files that are part of harness infrastructure, not project scaffolding. */
const HARNESS_CONFIG_FILES = new Set(['harness.config.json', 'AGENTS.md', '.harness/.gitignore']);

/** OS-generated files that may exist on a contributor's machine but are not part of any template. */
const IGNORED_TEMPLATE_FILES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);

function isHarnessConfigFile(relativePath: string): boolean {
  return HARNESS_CONFIG_FILES.has(relativePath);
}

/**
 * Known build/package configuration files that indicate a pre-existing project.
 * If any of these exist in the target directory, the project is considered existing
 * and only harness config files should be written (no scaffold files).
 */
const PROJECT_MARKERS = [
  'package.json',
  'pom.xml',
  'build.gradle',
  'build.gradle.kts',
  'pyproject.toml',
  'setup.py',
  'requirements.txt',
  'Pipfile',
  'go.mod',
  'Cargo.toml',
  'Makefile',
  'CMakeLists.txt',
  'meson.build',
];

function scoreDetectPatterns(
  targetDir: string,
  patterns: readonly { file: string; contains?: string | undefined }[]
): number {
  let score = 0;
  for (const pattern of patterns) {
    const filePath = path.join(targetDir, pattern.file);
    if (!fs.existsSync(filePath)) continue;
    if (!pattern.contains) {
      score++;
      continue;
    }
    const fd = fs.openSync(filePath, 'r');
    try {
      const buf = Buffer.alloc(65536);
      const bytesRead = fs.readSync(fd, buf, 0, 65536, 0);
      const content = buf.toString('utf-8', 0, bytesRead);
      if (content.includes(pattern.contains)) score++;
    } finally {
      fs.closeSync(fd);
    }
  }
  return score;
}

function applyLanguageDefaults(context: TemplateContext): TemplateContext {
  return {
    ...context,
    ...(context.language === 'python' &&
      context.pythonMinVersion === undefined && { pythonMinVersion: '3.10' }),
    ...(context.language === 'go' &&
      context.goModulePath === undefined && {
        goModulePath: `github.com/example/${context.projectName}`,
      }),
    ...(context.language === 'java' &&
      context.javaGroupId === undefined && {
        javaGroupId: `com.example.${context.projectName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()}`,
      }),
    ...(context.language === 'rust' &&
      context.rustEdition === undefined && { rustEdition: '2021' }),
  };
}

function mergeJsonBuffers(
  jsonBuffers: Map<string, Record<string, unknown>[]>
): Result<RenderedFile[], Error> {
  try {
    const results: RenderedFile[] = [];
    for (const [outputPath, jsons] of jsonBuffers) {
      let merged: Record<string, unknown> = {};
      for (const json of jsons) {
        merged =
          outputPath === 'package.json'
            ? mergePackageJson(merged, json)
            : deepMergeJson(merged, json);
      }
      results.push({ relativePath: outputPath, content: JSON.stringify(merged, null, 2) });
    }
    return Ok(results);
  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error);
    return Err(new Error(`JSON merge failed: ${msg}`));
  }
}

export class TemplateEngine {
  constructor(private templatesDir: string) {}

  listTemplates(): Result<TemplateMetadata[], Error> {
    try {
      const entries = fs.readdirSync(this.templatesDir, { withFileTypes: true });
      const templates: TemplateMetadata[] = [];
      for (const entry of entries) {
        if (!entry.isDirectory()) continue;
        const metaPath = path.join(this.templatesDir, entry.name, 'template.json');
        if (!fs.existsSync(metaPath)) continue;
        const raw = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
        const parsed = TemplateMetadataSchema.safeParse(raw);
        if (parsed.success) templates.push(parsed.data);
      }
      return Ok(templates);
    } catch (error) {
      return Err(
        new Error(
          `Failed to list templates: ${error instanceof Error ? error.message : String(error)}`
        )
      );
    }
  }

  resolveTemplate(
    level?: string,
    framework?: string,
    language?: string
  ): Result<ResolvedTemplate, Error> {
    // Non-JS language path: language-base -> optional framework overlay
    if (language && language !== 'typescript') {
      return this.resolveLanguageTemplate(language, framework);
    }

    // Existing JS/TS path: requires level
    if (!level) {
      return Err(new Error('Level is required for TypeScript/JavaScript templates'));
    }

    const levelDir = this.findTemplateDir(level, 'level');
    if (!levelDir) return Err(new Error(`Template not found for level: ${level}`));

    const metaPath = path.join(levelDir, 'template.json');
    const metaRaw = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
    const metaResult = TemplateMetadataSchema.safeParse(metaRaw);
    if (!metaResult.success)
      return Err(new Error(`Invalid template.json in ${level}: ${metaResult.error.message}`));

    const metadata = metaResult.data;
    let files: TemplateFile[] = [];

    if (metadata.extends) {
      const baseDir = path.join(this.templatesDir, metadata.extends);
      if (fs.existsSync(baseDir)) files = this.collectFiles(baseDir, metadata.extends);
    }

    const levelFiles = this.collectFiles(levelDir, level);
    files = this.mergeFileLists(files, levelFiles);

    let overlayMetadata: TemplateMetadata | undefined;
    if (framework) {
      const frameworkDir = this.findTemplateDir(framework, 'framework');
      if (!frameworkDir) return Err(new Error(`Framework template not found: ${framework}`));
      const fMetaPath = path.join(frameworkDir, 'template.json');
      const fMetaRaw = JSON.parse(fs.readFileSync(fMetaPath, 'utf-8'));
      const fMetaResult = TemplateMetadataSchema.safeParse(fMetaRaw);
      if (fMetaResult.success) overlayMetadata = fMetaResult.data;
      const frameworkFiles = this.collectFiles(frameworkDir, framework);
      files = this.mergeFileLists(files, frameworkFiles);
    }

    files = files.filter((f) => f.relativePath !== 'template.json');
    const resolved: ResolvedTemplate = { metadata, files };
    if (overlayMetadata !== undefined) resolved.overlayMetadata = overlayMetadata;
    return Ok(resolved);
  }

  render(template: ResolvedTemplate, context: TemplateContext): Result<RenderedFiles, Error> {
    const effectiveContext = applyLanguageDefaults(context);
    const rendered: RenderedFile[] = [];
    const jsonBuffers = new Map<string, Record<string, unknown>[]>();

    for (const file of template.files) {
      const result = this.renderFile(file, effectiveContext, jsonBuffers);
      if (!result.ok) return result;
      if (result.value) rendered.push(result.value);
    }

    const mergeResult = mergeJsonBuffers(jsonBuffers);
    if (!mergeResult.ok) return mergeResult;
    rendered.push(...mergeResult.value);

    return Ok({ files: rendered });
  }

  private renderFile(
    file: TemplateFile,
    context: TemplateContext,
    jsonBuffers: Map<string, Record<string, unknown>[]>
  ): Result<RenderedFile | null, Error> {
    const outputPath = file.relativePath.replace(/\.hbs$/, '');
    try {
      if (file.isHandlebars) {
        const raw = fs.readFileSync(file.absolutePath, 'utf-8');
        const compiled = Handlebars.compile(raw, { strict: true });
        const content = compiled(context);
        if (outputPath.endsWith('.json') && file.relativePath.endsWith('.json.hbs')) {
          if (!jsonBuffers.has(outputPath)) jsonBuffers.set(outputPath, []);
          jsonBuffers.get(outputPath)!.push(JSON.parse(content));
          return Ok(null);
        }
        return Ok({ relativePath: outputPath, content });
      }
      const content = fs.readFileSync(file.absolutePath, 'utf-8');
      return Ok({ relativePath: file.relativePath, content });
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      return Err(
        new Error(`Template render failed in ${file.sourceTemplate}/${file.relativePath}: ${msg}`)
      );
    }
  }

  write(
    files: RenderedFiles,
    targetDir: string,
    options: WriteOptions
  ): Result<WriteResult, Error> {
    try {
      const written: string[] = [];
      const skippedConfigs: string[] = [];
      const isNonJsLanguage = options.language && options.language !== 'typescript';

      for (const file of files.files) {
        const targetPath = path.join(targetDir, file.relativePath);
        const dir = path.dirname(targetPath);

        // Existing project: only write harness config files, skip project scaffold
        if (
          !options.overwrite &&
          options.existingProject &&
          !isHarnessConfigFile(file.relativePath)
        ) {
          continue;
        }

        // Skip non-JSON package configs for non-JS languages when file already exists
        if (
          !options.overwrite &&
          isNonJsLanguage &&
          NON_JSON_PACKAGE_CONFIGS.has(file.relativePath) &&
          fs.existsSync(targetPath)
        ) {
          skippedConfigs.push(file.relativePath);
          continue;
        }

        if (!options.overwrite && fs.existsSync(targetPath)) continue;
        fs.mkdirSync(dir, { recursive: true });
        fs.writeFileSync(targetPath, file.content);
        written.push(file.relativePath);
      }
      return Ok({ written, skippedConfigs });
    } catch (error) {
      return Err(
        new Error(
          `Failed to write files: ${error instanceof Error ? error.message : String(error)}`
        )
      );
    }
  }

  /**
   * Check whether the target directory already contains a project by looking for
   * common build/package configuration files. Returns true if any marker is found.
   */
  isExistingProject(targetDir: string): boolean {
    return PROJECT_MARKERS.some((marker) => fs.existsSync(path.join(targetDir, marker)));
  }

  detectFramework(targetDir: string): Result<DetectedFramework[], Error> {
    try {
      const templatesResult = this.listTemplates();
      if (!templatesResult.ok) return Err(templatesResult.error);

      const candidates: DetectedFramework[] = [];
      for (const meta of templatesResult.value) {
        if (!meta.detect || meta.detect.length === 0) continue;
        if (!meta.framework || !meta.language) continue;

        const score = scoreDetectPatterns(targetDir, meta.detect);
        if (score > 0) {
          candidates.push({
            framework: meta.framework,
            language: meta.language,
            score,
            templateName: meta.name,
          });
        }
      }

      candidates.sort((a, b) => b.score - a.score);
      return Ok(candidates);
    } catch (error) {
      return Err(
        new Error(
          `Framework detection failed: ${error instanceof Error ? error.message : String(error)}`
        )
      );
    }
  }

  private resolveLanguageTemplate(
    language: string,
    framework?: string
  ): Result<ResolvedTemplate, Error> {
    const baseName = `${language}-base`;
    const baseDir = this.findTemplateDir(baseName, 'name');
    if (!baseDir) return Err(new Error(`Language base template not found: ${baseName}`));

    const metaPath = path.join(baseDir, 'template.json');
    const metaRaw = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
    const metaResult = TemplateMetadataSchema.safeParse(metaRaw);
    if (!metaResult.success)
      return Err(new Error(`Invalid template.json in ${baseName}: ${metaResult.error.message}`));

    const metadata = metaResult.data;
    let files = this.collectFiles(baseDir, baseName);

    let overlayMetadata: TemplateMetadata | undefined;
    if (framework) {
      const frameworkDir = this.findTemplateDir(framework, 'framework');
      if (!frameworkDir) return Err(new Error(`Framework template not found: ${framework}`));
      const fMetaPath = path.join(frameworkDir, 'template.json');
      const fMetaRaw = JSON.parse(fs.readFileSync(fMetaPath, 'utf-8'));
      const fMetaResult = TemplateMetadataSchema.safeParse(fMetaRaw);
      if (fMetaResult.success) overlayMetadata = fMetaResult.data;
      const frameworkFiles = this.collectFiles(frameworkDir, framework);
      files = this.mergeFileLists(files, frameworkFiles);
    }

    files = files.filter((f) => f.relativePath !== 'template.json');
    const resolved: ResolvedTemplate = { metadata, files };
    if (overlayMetadata !== undefined) resolved.overlayMetadata = overlayMetadata;
    return Ok(resolved);
  }

  private findTemplateDir(name: string, type: 'level' | 'framework' | 'name'): string | null {
    const entries = fs.readdirSync(this.templatesDir, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      const metaPath = path.join(this.templatesDir, entry.name, 'template.json');
      if (!fs.existsSync(metaPath)) continue;
      const raw = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
      const parsed = TemplateMetadataSchema.safeParse(raw);
      if (!parsed.success) continue;
      const fieldValue = parsed.data[type];
      if (fieldValue === name) return path.join(this.templatesDir, entry.name);
    }
    return null;
  }

  private collectFiles(dir: string, sourceName: string): TemplateFile[] {
    const files: TemplateFile[] = [];
    const walk = (currentDir: string): void => {
      const entries = fs.readdirSync(currentDir, { withFileTypes: true });
      for (const entry of entries) {
        if (IGNORED_TEMPLATE_FILES.has(entry.name)) continue;
        const fullPath = path.join(currentDir, entry.name);
        if (entry.isDirectory()) {
          walk(fullPath);
        } else {
          files.push({
            relativePath: path.relative(dir, fullPath).replace(/\\/g, '/'),
            absolutePath: fullPath,
            isHandlebars: entry.name.endsWith('.hbs'),
            sourceTemplate: sourceName,
          });
        }
      }
    };
    walk(dir);
    return files;
  }

  private mergeFileLists(base: TemplateFile[], overlay: TemplateFile[]): TemplateFile[] {
    const map = new Map<string, TemplateFile>();
    for (const file of base) map.set(file.relativePath, file);
    for (const file of overlay) {
      if (file.relativePath.endsWith('.json.hbs')) {
        const baseKey = base.find((f) => f.relativePath === file.relativePath);
        if (baseKey) {
          map.set(`__overlay__${file.relativePath}`, file);
        } else {
          map.set(file.relativePath, file);
        }
      } else {
        map.set(file.relativePath, file);
      }
    }
    return Array.from(map.values());
  }
}
