// INSTALADOR DE UN PROGRAMA DE GUSTITO XPRESS.
//
// Escrito en C# y compilado con el compilador que ya trae Windows: pesa
// kilobytes, no los 90 MB de un programa empaquetado con su propio motor. Un
// instalador que pesa lo mismo que lo que instala no sirve de nada.
//
// ⚠ CADA INSTALADOR TRAE UN SOLO PROGRAMA (se fija al compilar). La Estacion
// Fiscal es el sistema de facturacion, sujeto a homologacion ante el SENIAT;
// el Cerebrito es gestion del negocio y no emite documentos fiscales.
// Entregarlos juntos difuminaria esa frontera y ademas encerraria a la
// Estacion dentro de Gustito, cuando le sirve a cualquier comercio.
//
// Que hace: baja del canal publicado la ultima version, comprueba que llego
// completa, la deja en su carpeta y la enciende. De los accesos del escritorio
// y del arranque con Windows se encarga el propio programa: aqui no se toca el
// registro.
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Windows.Forms;

static class Marca {
  public static readonly Color Fondo   = ColorTranslator.FromHtml("#0B0D14");
  public static readonly Color Panel   = ColorTranslator.FromHtml("#131624");
  public static readonly Color Panel2  = ColorTranslator.FromHtml("#1A1E30");
  public static readonly Color Borde   = ColorTranslator.FromHtml("#2A2F4A");
  public static readonly Color Texto   = Color.White;
  public static readonly Color Texto2  = ColorTranslator.FromHtml("#D6DAEA");
  public static readonly Color Oro     = ColorTranslator.FromHtml("#FBBF24");
  public static readonly Color Verde   = ColorTranslator.FromHtml("#34D399");
  public static readonly Color Rojo    = ColorTranslator.FromHtml("#F87171");
}

class Producto {
  public string Nombre, Lema, Sigla, Resumen, Despues, Carpeta, Exe, Canal;
  public string[] Incluye;
}

static class Catalogo {
  public static Producto Actual() {
#if PRODUCTO_CEREBRITO
    return new Producto {
      Nombre  = "Cerebrito",
      Lema    = "La cocina y el salón, en orden.",
      Sigla   = "CG",
      Resumen = "El programa que corre en la laptop del local: imprime la comanda de cada puesto y levanta el punto de venta de las tablets del salón.",
      Incluye = new string[] {
        "Comandas impresas en la impresora de cada puesto",
        "Punto de venta para las tablets del salón, por la red del local",
        "Sigue trabajando sin internet y se pone al día solo"
      },
      Despues = "Al abrirlo entrarás con la cuenta del negocio y elegirás sus impresoras.",
      Carpeta = "Cerebrito",
      Exe     = "Cerebrito-Gustito.exe",
      Canal   = "cerebrito"
    };
#else
    return new Producto {
      Nombre  = "Estación Fiscal",
      Lema    = "Cada venta, sellada y en regla.",
      Sigla   = "EF",
      Resumen = "El sistema de facturación del negocio: arma el documento con su desglose de impuestos, lo asienta en un libro inalterable con numeración consecutiva y lo transmite a la imprenta digital autorizada.",
      Incluye = new string[] {
        "Libro fiscal inalterable, con numeración única y consecutiva",
        "Transmisión inmediata y entrega de la factura al cliente",
        "Consulta de solo lectura para el SENIAT, con su propia clave",
        "Respaldo cifrado fuera del equipo y contingencia con talonario"
      },
      Despues = "Al abrirla por primera vez le pondrás la clave del dueño y cargarás los datos del negocio.",
      Carpeta = "EstacionFiscal",
      Exe     = "Estacion-Fiscal.exe",
      Canal   = "estacion"
    };
#endif
  }
}

/// La mecanica, aparte de la ventana para poder probarla desde la consola.
static class Trabajo {
  public static string BaseCanal = Environment.GetEnvironmentVariable("GX_CANAL") ?? "https://img.gustitoxpress.app";
  public static string Destino   = Environment.GetEnvironmentVariable("GX_DESTINO") ?? "C:\\Gustito";

  public static string Carpeta(Producto p) { return Path.Combine(Destino, p.Carpeta); }
  public static string RutaExe(Producto p) { return Path.Combine(Carpeta(p), p.Exe); }
  public static bool YaInstalado(Producto p) { return File.Exists(RutaExe(p)); }

  /// Lo que publica hoy el canal. El archivo trae dos campos: se leen sin
  /// arrastrar una libreria de JSON para no engordar el instalador.
  public static bool Publicado(Producto p, out string version, out string url) {
    version = null; url = null;
    try {
      ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // TLS 1.2
      using (var cliente = new WebClient()) {
        cliente.Headers.Add("cache-control", "no-cache");
        string crudo = cliente.DownloadString(BaseCanal + "/" + p.Canal + "/version.json");
        var mv = Regex.Match(crudo, "\"version\"\\s*:\\s*\"([^\"]+)\"");
        var mu = Regex.Match(crudo, "\"url\"\\s*:\\s*\"([^\"]+)\"");
        if (!mv.Success || !mu.Success) return false;
        version = mv.Groups[1].Value; url = mu.Groups[1].Value;
        return true;
      }
    } catch { return false; }
  }

  /// Baja el programa. Se escribe en un archivo aparte y se renombra al final:
  /// si el internet del local se corta, no queda un programa a medias
  /// haciendose pasar por el bueno.
  public static void Descargar(Producto p, string url, Action<int> avance) {
    Directory.CreateDirectory(Carpeta(p));
    string destino = RutaExe(p), temporal = destino + ".bajando";
    try {
      ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
      var pedido = (HttpWebRequest)WebRequest.Create(url);
      pedido.Timeout = 60000;
      using (var respuesta = (HttpWebResponse)pedido.GetResponse())
      using (var entrada = respuesta.GetResponseStream())
      using (var salida = new FileStream(temporal, FileMode.Create, FileAccess.Write)) {
        long esperado = respuesta.ContentLength, leidos = 0;
        byte[] buf = new byte[131072];
        int n;
        while ((n = entrada.Read(buf, 0, buf.Length)) > 0) {
          salida.Write(buf, 0, n);
          leidos += n;
          if (esperado > 0 && avance != null) avance((int)Math.Min(99, leidos * 100 / esperado));
        }
        salida.Flush();
        if (esperado > 0 && leidos != esperado)
          throw new Exception("La descarga quedó incompleta (" + leidos + " de " + esperado + " bytes).");
      }
      long real = new FileInfo(temporal).Length;
      if (real < 10000000) throw new Exception("Lo descargado no parece un programa completo.");
      if (File.Exists(destino)) File.Delete(destino);
      File.Move(temporal, destino);
      if (avance != null) avance(100);
    } catch (Exception e) {
      // Nunca se deja un archivo a medias en la carpeta del negocio.
      try { if (File.Exists(temporal)) File.Delete(temporal); } catch { }
      string dicho = e.Message;
      if (!dicho.StartsWith("La descarga") && !dicho.StartsWith("Lo descargado"))
        dicho = "La descarga quedó incompleta.";
      throw new Exception(dicho + " Revisa la conexión a internet e intenta de nuevo.");
    }
  }

  /// Lo enciende. De sus accesos y su arranque con Windows se encarga el.
  public static void Arrancar(Producto p) {
    var inicio = new ProcessStartInfo(RutaExe(p));
    inicio.WorkingDirectory = Carpeta(p);
    inicio.UseShellExecute = true;
    Process.Start(inicio);
  }
}

class Ventana : Form {
  readonly Producto p;
  Label titulo, estado, versionDicha;
  Button accion, cerrar;
  Panel barra, relleno;

  public Ventana(Producto producto) {
    p = producto;
    Text = "Instalar " + p.Nombre;
    ClientSize = new Size(980, 620);
    StartPosition = FormStartPosition.CenterScreen;
    FormBorderStyle = FormBorderStyle.FixedSingle;
    MaximizeBox = false;
    BackColor = Marca.Fondo;
    Font = new Font("Segoe UI", 9.75f);
    try { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); } catch { }
    Armar();
  }

  static Label Texto(string t, Font f, Color c, int x, int y, int ancho, int alto) {
    var l = new Label();
    l.Text = t; l.Font = f; l.ForeColor = c;
    l.Location = new Point(x, y); l.Size = new Size(ancho, alto);
    l.BackColor = Color.Transparent;
    return l;
  }

  void Armar() {
    // --- Lado de la marca (izquierda) ---
    var marca = new Panel();
    marca.Bounds = new Rectangle(0, 0, 470, 620);
    marca.BackColor = Marca.Fondo;
    marca.Paint += delegate(object s, PaintEventArgs e) {
      var g = e.Graphics;
      g.SmoothingMode = SmoothingMode.AntiAlias;
      using (var brocha = new LinearGradientBrush(new Rectangle(176, 170, 118, 118), Marca.Oro, ColorTranslator.FromHtml("#B45309"), 55f))
      using (var camino = new GraphicsPath()) {
        int x = 176, y = 170, lado = 118, r = 26;
        camino.AddArc(x, y, r * 2, r * 2, 180, 90);
        camino.AddArc(x + lado - r * 2, y, r * 2, r * 2, 270, 90);
        camino.AddArc(x + lado - r * 2, y + lado - r * 2, r * 2, r * 2, 0, 90);
        camino.AddArc(x, y + lado - r * 2, r * 2, r * 2, 90, 90);
        camino.CloseFigure();
        g.FillPath(brocha, camino);
      }
      using (var f = new Font("Segoe UI", 30f, FontStyle.Bold))
      using (var t = new SolidBrush(Marca.Fondo)) {
        var tam = g.MeasureString(p.Sigla, f);
        g.DrawString(p.Sigla, f, t, 176 + (118 - tam.Width) / 2, 170 + (118 - tam.Height) / 2);
      }
      using (var f = new Font("Segoe UI", 19f, FontStyle.Bold))
      using (var t = new SolidBrush(Marca.Texto)) {
        var tam = g.MeasureString(p.Nombre, f);
        g.DrawString(p.Nombre, f, t, (470 - tam.Width) / 2, 316);
      }
      using (var f = new Font("Consolas", 9.5f))
      using (var t = new SolidBrush(Marca.Texto2)) {
        var tam = g.MeasureString(p.Lema, f);
        g.DrawString(p.Lema, f, t, (470 - tam.Width) / 2, 356);
      }
      using (var t = new SolidBrush(Marca.Oro)) g.FillRectangle(t, 212, 392, 46, 3);
      using (var lapiz = new Pen(Marca.Borde)) g.DrawLine(lapiz, 469, 0, 469, 620);
    };
    Controls.Add(marca);

    // --- Tarjeta de accion (derecha) ---
    var tarjeta = new Panel();
    tarjeta.Bounds = new Rectangle(506, 74, 438, 472);
    tarjeta.BackColor = Marca.Panel;
    tarjeta.Paint += delegate(object s, PaintEventArgs e) {
      using (var lapiz = new Pen(Marca.Borde)) e.Graphics.DrawRectangle(lapiz, 0, 0, tarjeta.Width - 1, tarjeta.Height - 1);
    };
    Controls.Add(tarjeta);

    bool instalado = Trabajo.YaInstalado(p);
    titulo = Texto(instalado ? "Ya está instalado" : "Instalar en este equipo",
      new Font("Segoe UI", 15f, FontStyle.Bold), Marca.Texto, 28, 26, 380, 30);
    tarjeta.Controls.Add(titulo);

    tarjeta.Controls.Add(Texto(p.Resumen, new Font("Segoe UI", 9f), Marca.Texto2, 28, 62, 382, 76));

    int fila = 146;
    foreach (string linea in p.Incluye) {
      tarjeta.Controls.Add(Texto("\u2022  " + linea, new Font("Segoe UI", 9f), Marca.Texto2, 30, fila, 380, 30));
      fila += 26;
    }

    fila += 12;
    tarjeta.Controls.Add(Texto("Se instala en", new Font("Segoe UI", 8.5f), Marca.Texto2, 28, fila, 140, 18));
    tarjeta.Controls.Add(Texto(Trabajo.Carpeta(p), new Font("Consolas", 8.5f), Marca.Texto, 168, fila, 242, 18));
    fila += 22;
    tarjeta.Controls.Add(Texto("Versión a instalar", new Font("Segoe UI", 8.5f), Marca.Texto2, 28, fila, 140, 18));
    versionDicha = Texto("consultando...", new Font("Consolas", 8.5f), Marca.Texto, 168, fila, 242, 18);
    tarjeta.Controls.Add(versionDicha);
    fila += 30;

    barra = new Panel();
    barra.Bounds = new Rectangle(28, fila, 382, 9);
    barra.BackColor = Marca.Panel2;
    barra.Visible = false;
    relleno = new Panel();
    relleno.Bounds = new Rectangle(0, 0, 0, 9);
    relleno.BackColor = Marca.Oro;
    barra.Controls.Add(relleno);
    tarjeta.Controls.Add(barra);
    fila += 18;

    estado = Texto("", new Font("Segoe UI", 8.75f), Marca.Texto2, 28, fila, 382, 54);
    tarjeta.Controls.Add(estado);

    accion = new Button();
    accion.Text = instalado ? "Volver a instalar" : "Instalar";
    accion.Bounds = new Rectangle(28, 400, 382, 42);
    accion.FlatStyle = FlatStyle.Flat;
    accion.FlatAppearance.BorderSize = 0;
    accion.BackColor = Marca.Oro;
    accion.ForeColor = Marca.Fondo;
    accion.Font = new Font("Segoe UI", 10.5f, FontStyle.Bold);
    accion.Cursor = Cursors.Hand;
    accion.Enabled = false;
    accion.Click += delegate { Instalar(); };
    tarjeta.Controls.Add(accion);

    cerrar = new Button();
    cerrar.Text = "Cerrar el instalador";
    cerrar.Bounds = new Rectangle(28, 400, 382, 42);
    cerrar.FlatStyle = FlatStyle.Flat;
    cerrar.FlatAppearance.BorderColor = Marca.Borde;
    cerrar.BackColor = Marca.Panel;
    cerrar.ForeColor = Marca.Texto2;
    cerrar.Font = new Font("Segoe UI", 10f);
    cerrar.Cursor = Cursors.Hand;
    cerrar.Visible = false;
    cerrar.Click += delegate { Close(); };
    tarjeta.Controls.Add(cerrar);

    Shown += delegate { ConsultarCanal(); };
  }

  string urlDescarga;

  void ConsultarCanal() {
    var hilo = new System.Threading.Thread(delegate() {
      string v, u;
      bool ok = Trabajo.Publicado(p, out v, out u);
      Invoke((MethodInvoker)delegate {
        if (ok) {
          urlDescarga = u;
          versionDicha.Text = v;
          accion.Enabled = true;
        } else {
          versionDicha.Text = "sin conexión";
          versionDicha.ForeColor = Marca.Rojo;
          estado.ForeColor = Marca.Rojo;
          estado.Text = "No se pudo consultar el canal de descargas. Revisa la conexión a internet y vuelve a abrir el instalador.";
        }
      });
    });
    hilo.IsBackground = true;
    hilo.Start();
  }

  void Instalar() {
    accion.Enabled = false;
    accion.Text = "Instalando...";
    barra.Visible = true;
    estado.ForeColor = Marca.Texto2;
    estado.Text = "Descargando el programa...";
    var hilo = new System.Threading.Thread(delegate() {
      try {
        Trabajo.Descargar(p, urlDescarga, delegate(int pc) {
          Invoke((MethodInvoker)delegate {
            relleno.Width = barra.Width * pc / 100;
            estado.Text = "Descargando el programa... " + pc + "%";
          });
        });
        Invoke((MethodInvoker)delegate { estado.Text = "Encendiendo el programa por primera vez..."; });
        Trabajo.Arrancar(p);
        System.Threading.Thread.Sleep(2500);
        Invoke((MethodInvoker)delegate {
          titulo.Text = "Listo";
          estado.ForeColor = Marca.Verde;
          estado.Text = "Quedó instalado y encendido. Su acceso está en el escritorio y arranca solo con Windows. " + p.Despues;
          accion.Visible = false;
          cerrar.Visible = true;
        });
      } catch (Exception e) {
        string dicho = e.Message;
        Invoke((MethodInvoker)delegate {
          estado.ForeColor = Marca.Rojo;
          estado.Text = dicho;
          accion.Enabled = true;
          accion.Text = "Intentar de nuevo";
        });
      }
    });
    hilo.IsBackground = true;
    hilo.Start();
  }
}

static class Programa {
  [STAThread]
  static int Main(string[] args) {
    var p = Catalogo.Actual();
    // Modo consola: lo usan las pruebas para ejercitar el instalador de verdad
    // (descarga cortada, descarga completa) sin abrir ventana.
    if (Array.IndexOf(args, "--consola") >= 0) {
      // La consola de Windows arranca en una codificacion vieja: sin esto, un
      // mensaje con tildes sale ilegible en el archivo de registro del negocio.
      try { Console.OutputEncoding = System.Text.Encoding.UTF8; } catch { }
      try {
        string v, u;
        if (!Trabajo.Publicado(p, out v, out u)) { Console.WriteLine("SIN CANAL"); return 2; }
        Console.WriteLine("VERSION " + v);
        Trabajo.Descargar(p, u, null);
        Console.WriteLine("INSTALADO " + Trabajo.RutaExe(p));
        return 0;
      } catch (Exception e) {
        Console.WriteLine("PROBLEMA " + e.Message);
        return 1;
      }
    }
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Ventana(p));
    return 0;
  }
}
