// Shared API helpers behind the wasm and non-wasm entrypoints.
//
// `host.Expose` (under //go:build js) and the cmd's native `main.go` both
// call into these helpers so build/check/transform behavior stays
// bit-identical across the two surfaces.
package host

import (
  "encoding/json"
  "fmt"
  "path/filepath"
  "strings"

  shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
  shimtspath "github.com/microsoft/typescript-go/shim/tspath"

  "github.com/samchon/ttsc/packages/ttsc/driver"
)

// Build runs `ttsc build`-shaped emit: load the project, emit JS, return the
// emitted text map + diagnostics as JSON. `cwd` must be an absolute path
// inside the host filesystem; `tsconfigPath` may be relative to cwd.
//
// The loaded Program is closed on every return after acquisition. Compiler
// diagnostics still accompany emitted files; code 2 reports compiler errors,
// while code 3 reports emit or serialization failures.
//
// @evidence contracts/common.md#principled-implementation LoadProgram and EmitAll own compiler behavior; encoding/json projects their results through the documented envelope.
// @evidence contracts/common.md#clear-and-simple-design One emit pipeline owns project acquisition, supported WriteFile capture and diagnostics; shared projection helpers centralize path and diagnostic policy.
// @evidence contracts/common.md#prohibited-implementation-shortcuts Emit uses supported compiler hooks, without patching foreign emitters or synthesizing expected outputs.
// @evidence contracts/common.md#meaningful-documentation Native paragraphs explain paths, ownership and failure stages under the documentation skill's rationale guidance.
// @evidence contracts/portability.md#os-neutral-implementation LoadProgram owns filesystem access through the driver's supported host. Native filepath.Rel identifies inside/outside destinations, including volume differences, and ToSlash changes JSON spelling without lowercasing identity.
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Build closes its Program before returning and transfers the output to the caller, so it retains nothing.
// @evidenceExclude contracts/performance.md#efficient-algorithms Build delegates parsing, checking and emit to the driver and adds one pass over the diagnostics and the emitted output map.
// @evidenceExclude contracts/performance.md#reuse-equivalent-work Build loads a fresh Program per call because the virtual filesystem may have changed; sharing a loaded Program is the snapshot verbs' purpose.
func Build(cwd, tsconfigPath string) ([]byte, int, error) {
  prog, diags, err := driver.LoadProgram(cwd, tsconfigPath, driver.LoadProgramOptions{
    ForceEmit: true,
  })
  if err != nil {
    return nil, 2, err
  }
  if prog != nil {
    defer prog.Close()
    diags = append(diags, prog.Diagnostics()...)
  }
  output := map[string]string{}
  if prog != nil {
    rewrites := driver.NewRewriteSet()
    writeFile := shimcompiler.WriteFile(
      func(fileName shimtspath.RootedFilePath, text string, _ *shimcompiler.WriteFileData) error {
        output[apiOutputKey(cwd, string(fileName))] = text
        return nil
      },
    )
    _, emitDiags, eerr := prog.EmitAll(rewrites, writeFile)
    if eerr != nil {
      return nil, 3, fmt.Errorf("emit failed: %w", eerr)
    }
    diags = append(diags, emitDiags...)
  }
  result := CompileResult{
    Diagnostics: make([]CompileDiagnostic, 0, len(diags)),
    Output:      output,
  }
  for _, d := range diags {
    result.Diagnostics = append(result.Diagnostics, toAPIDiagnostic(d))
  }
  data, err := json.Marshal(result)
  if err != nil {
    return nil, 3, fmt.Errorf("result marshal failed: %w", err)
  }
  code := 0
  if driver.CountErrors(diags) > 0 {
    code = 2
  }
  return data, code, nil
}

// Check runs the typecheck pipeline without emitting JS. The returned JSON
// has the same shape as Build but with an empty `output` map.
//
// @evidence contracts/common.md#principled-implementation ForceNoEmit selects the driver's check pipeline and a deferred Close owns the loaded program.
// @evidence contracts/common.md#clear-and-simple-design The no-emit entry keeps loading and diagnostic projection explicit while sharing DTO and path helpers with the emit entry.
// @evidence contracts/common.md#prohibited-implementation-shortcuts No-emit is requested from the compiler rather than hiding an unnecessary emit behind an empty result.
// @evidence contracts/common.md#meaningful-documentation Native comments explain no-emit and payload equivalence under the documentation skill's context guidance.
// @evidence contracts/portability.md#os-neutral-implementation The driver resolves cwd/config through its filesystem host; diagnostic paths use filepath.ToSlash only for wire spelling. The adapter neither guesses case policy nor constructs a shell command.
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Check closes its Program before returning and transfers the diagnostics to the caller, so it retains nothing.
// @evidenceExclude contracts/performance.md#efficient-algorithms Check delegates loading and checking to the driver and adds one pass over the diagnostics.
// @evidenceExclude contracts/performance.md#reuse-equivalent-work Check loads a fresh Program per call because the virtual filesystem may have changed; sharing a loaded Program is the snapshot verbs' purpose.
func Check(cwd, tsconfigPath string) ([]byte, int, error) {
  prog, diags, err := driver.LoadProgram(cwd, tsconfigPath, driver.LoadProgramOptions{
    ForceNoEmit: true,
  })
  if err != nil {
    return nil, 2, err
  }
  if prog != nil {
    defer prog.Close()
    diags = append(diags, prog.Diagnostics()...)
  }
  result := CompileResult{
    Diagnostics: make([]CompileDiagnostic, 0, len(diags)),
    Output:      map[string]string{},
  }
  for _, d := range diags {
    result.Diagnostics = append(result.Diagnostics, toAPIDiagnostic(d))
  }
  data, err := json.Marshal(result)
  if err != nil {
    return nil, 3, fmt.Errorf("result marshal failed: %w", err)
  }
  code := 0
  if driver.CountErrors(diags) > 0 {
    code = 2
  }
  return data, code, nil
}

// Transform returns every source file the program saw, keyed by
// project-relative path. The playground uses this to render the TS view
// after source rewrites (e.g. paths rewriting) have been applied.
//
// Sources inside cwd use relative keys; outside sources retain absolute slash
// paths. The invocation closes the loaded Program after copying its source text.
//
// @evidence contracts/common.md#principled-implementation LoadProgram supplies post-plugin SourceFiles and encoding/json preserves their source-text projection.
// @evidence contracts/common.md#clear-and-simple-design The source projection is separate from emit, and common path/diagnostic helpers keep their shared representation policy in one place.
// @evidence contracts/common.md#prohibited-implementation-shortcuts The program's own text is authoritative, without a secondary emitter-based reconstruction.
// @evidence contracts/common.md#meaningful-documentation Separate paragraphs explain stage, path convention and ownership under the documentation skill.
// @evidence contracts/portability.md#os-neutral-implementation Source identities come from LoadProgram, then the shared filepath relative/absolute mapping preserves native case and handles outside/other-volume paths before slash serialization.
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Transform closes its Program after copying the source text, so it retains nothing.
// @evidenceExclude contracts/performance.md#efficient-algorithms Transform delegates loading to the driver and adds one pass over the source files and diagnostics.
// @evidenceExclude contracts/performance.md#reuse-equivalent-work Transform loads a fresh Program per call because the virtual filesystem may have changed; sharing a loaded Program is the snapshot verbs' purpose.
func Transform(cwd, tsconfigPath string) ([]byte, int, error) {
  prog, diags, err := driver.LoadProgram(cwd, tsconfigPath, driver.LoadProgramOptions{
    ForceNoEmit: true,
  })
  if err != nil {
    return nil, 2, err
  }
  typescript := map[string]string{}
  if prog != nil {
    defer prog.Close()
    for _, file := range prog.SourceFiles() {
      typescript[apiOutputKey(cwd, string(file.FileName()))] = file.Text()
    }
    diags = append(diags, prog.Diagnostics()...)
  }
  result := TransformResult{
    Diagnostics: make([]CompileDiagnostic, 0, len(diags)),
    TypeScript:  typescript,
  }
  for _, d := range diags {
    result.Diagnostics = append(result.Diagnostics, toAPIDiagnostic(d))
  }
  data, err := json.Marshal(result)
  if err != nil {
    return nil, 3, fmt.Errorf("result marshal failed: %w", err)
  }
  code := 0
  if driver.CountErrors(diags) > 0 {
    code = 2
  }
  return data, code, nil
}

// toAPIDiagnostic converts a driver.Diagnostic into the JSON-serialisable
// CompileDiagnostic. File paths are normalised to forward slashes so the JS
// host can compare them against its own MemFS paths.
func toAPIDiagnostic(diag driver.Diagnostic) CompileDiagnostic {
  var file *string
  if diag.File != "" {
    value := filepath.ToSlash(diag.File)
    file = &value
  }
  category := "error"
  if diag.Severity == driver.SeverityWarning {
    category = "warning"
  }
  return CompileDiagnostic{
    File:        file,
    Category:    category,
    Code:        diag.Code,
    Start:       diag.Start,
    Length:      diag.Length,
    Line:        diag.Line,
    Character:   diag.Column,
    MessageText: diag.Message,
  }
}

// apiOutputKey returns the map key used in CompileResult.Output and
// TransformResult.TypeScript. Paths inside cwd are made project-relative;
// paths outside cwd (e.g. lib files) are returned as absolute slash paths.
func apiOutputKey(cwd, fileName string) string {
  rel, err := filepath.Rel(cwd, fileName)
  if err != nil || isOutsideRelativePath(rel) {
    return filepath.ToSlash(fileName)
  }
  return filepath.ToSlash(rel)
}

// isOutsideRelativePath reports whether rel escapes the base directory (i.e.
// starts with ".."). Such paths should not be presented as project-relative
// keys to the JS caller.
func isOutsideRelativePath(rel string) bool {
  return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
