#!/usr/bin/env python3

import hashlib
import zipfile
import tkinter as tk
from tkinter import filedialog, messagebox
from pathlib import Path

APP_NAME = "LSOS Builder"
APP_VERSION = "0.5"
FORMAT_VERSION = "0.5"


def sha256_file(path: Path) -> str:
    h = hashlib.sha256()

    with path.open("rb") as f:
        while True:
            chunk = f.read(1024 * 1024)
            if not chunk:
                break
            h.update(chunk)

    return h.hexdigest()


class LSOSBuilder:
    def __init__(self, root):
        self.root = root
        self.project_dir = None

        self.root.title(f"{APP_NAME} v{APP_VERSION}")
        self.root.geometry("650x650")

        self.name = tk.StringVar()
        self.version = tk.StringVar(value="1.0")
        self.architecture = tk.StringVar(value="x86_64")
        self.source = tk.StringVar()
        self.cdn = tk.StringVar()
        self.default_user = tk.StringVar()
        self.signature_enabled = tk.BooleanVar(value=False)

        self.make_ui()

    def make_ui(self):
        tk.Label(
            self.root,
            text="LSOS Builder",
            font=("Arial", 26, "bold")
        ).pack(pady=(20, 5))

        tk.Label(
            self.root,
            text=f"Lightweight Setup OS Builder v{APP_VERSION}"
        ).pack(pady=(0, 20))

        form = tk.Frame(self.root)
        form.pack(fill="x", padx=30)

        self.add_entry(form, "Name", self.name)
        self.add_entry(form, "Version", self.version)
        self.add_entry(form, "Architecture", self.architecture)
        self.add_entry(form, "Source", self.source)
        self.add_entry(form, "CDN", self.cdn)
        self.add_entry(form, "Default User", self.default_user)

        tk.Checkbutton(
            form,
            text="Enable LSING signature",
            variable=self.signature_enabled
        ).pack(anchor="w", pady=10)

        buttons = tk.Frame(self.root)
        buttons.pack(pady=20)

        tk.Button(
            buttons,
            text="New Project",
            width=18,
            command=self.new_project
        ).grid(row=0, column=0, padx=5, pady=5)

        tk.Button(
            buttons,
            text="Open Project",
            width=18,
            command=self.open_project
        ).grid(row=0, column=1, padx=5, pady=5)

        tk.Button(
            buttons,
            text="Save Project",
            width=18,
            command=self.save_project
        ).grid(row=1, column=0, padx=5, pady=5)

        tk.Button(
            buttons,
            text="BUILD .LSOS",
            width=18,
            command=self.build_lsos
        ).grid(row=1, column=1, padx=5, pady=5)

        self.status = tk.Label(
            self.root,
            text="Ready",
            anchor="w",
            relief="sunken"
        )
        self.status.pack(side="bottom", fill="x")

    def add_entry(self, parent, label, variable):
        tk.Label(
            parent,
            text=label,
            anchor="w"
        ).pack(fill="x")

        tk.Entry(
            parent,
            textvariable=variable
        ).pack(fill="x", pady=(2, 10))

    def new_project(self):
        directory = filedialog.askdirectory(
            title="Select LSOS project folder"
        )

        if not directory:
            return

        self.project_dir = Path(directory)

        self.create_project_files()

        self.status.config(
            text=f"Project created: {self.project_dir}"
        )

        messagebox.showinfo(
            APP_NAME,
            "Project created successfully."
        )

    def create_project_files(self):
        self.project_dir.mkdir(
            parents=True,
            exist_ok=True
        )

        (self.project_dir / "assets").mkdir(
            exist_ok=True
        )

        (self.project_dir / "signing").mkdir(
            exist_ok=True
        )

        version_txt = self.project_dir / "version.txt"

        if not version_txt.exists():
            version_txt.write_text(
                """name=
Source=
version=1.0
default-user=
cdn=
architecture=x86_64
""",
                encoding="utf-8"
            )

        build_prop = self.project_dir / "build.prop"

        if not build_prop.exists():
            build_prop.write_text(
                f"""lsos.version={FORMAT_VERSION}
builder.version={APP_VERSION}
format.version={FORMAT_VERSION}
architecture=x86_64
install.mode=no-usb
signature.enabled=false
""",
                encoding="utf-8"
            )

        signature_file = self.project_dir / "signature.lsing"

        if not signature_file.exists():
            signature_file.write_text(
                """LSING/0.5
signed=false
issuer=
key_id=
algorithm=ed25519
signature=
""",
                encoding="utf-8"
            )

        magic_file = self.project_dir / "LSOS.MAGIC"

        if not magic_file.exists():
            magic_file.write_text(
                f"""LSOS
FORMAT={FORMAT_VERSION}
""",
                encoding="utf-8"
            )

    def save_project(self):
        if self.project_dir is None:
            messagebox.showerror(
                APP_NAME,
                "No project is open."
            )
            return False

        version_txt = f"""name={self.name.get().strip()}
Source={self.source.get().strip()}
version={self.version.get().strip()}
default-user={self.default_user.get().strip()}
cdn={self.cdn.get().strip()}
architecture={self.architecture.get().strip()}
"""

        (self.project_dir / "version.txt").write_text(
            version_txt,
            encoding="utf-8"
        )

        build_prop = f"""lsos.version={FORMAT_VERSION}
builder.version={APP_VERSION}
format.version={FORMAT_VERSION}
architecture={self.architecture.get().strip()}
install.mode=no-usb
signature.enabled={str(self.signature_enabled.get()).lower()}
"""

        (self.project_dir / "build.prop").write_text(
            build_prop,
            encoding="utf-8"
        )

        self.status.config(
            text="Project saved"
        )

        return True

    def open_project(self):
        directory = filedialog.askdirectory(
            title="Open LSOS project"
        )

        if not directory:
            return

        project = Path(directory)
        version_file = project / "version.txt"

        if not version_file.exists():
            messagebox.showerror(
                APP_NAME,
                "This is not an LSOS project: version.txt not found."
            )
            return

        self.project_dir = project

        values = {}

        for line in version_file.read_text(
            encoding="utf-8"
        ).splitlines():
            if "=" not in line:
                continue

            key, value = line.split("=", 1)
            values[key.strip()] = value.strip()

        self.name.set(
            values.get("name", "")
        )

        self.source.set(
            values.get("Source", "")
        )

        self.version.set(
            values.get("version", "1.0")
        )

        self.default_user.set(
            values.get("default-user", "")
        )

        self.cdn.set(
            values.get("cdn", "")
        )

        self.architecture.set(
            values.get("architecture", "x86_64")
        )

        self.status.config(
            text=f"Opened: {self.project_dir}"
        )

    def validate_project(self):
        if self.project_dir is None:
            return "No project is open."

        if not self.name.get().strip():
            return "Name is required."

        if not self.version.get().strip():
            return "Version is required."

        if not self.architecture.get().strip():
            return "Architecture is required."

        if not self.cdn.get().strip():
            return "CDN is required."

        return None

    def build_lsos(self):
        error = self.validate_project()

        if error:
            messagebox.showerror(
                APP_NAME,
                error
            )
            return

        if not self.save_project():
            return

        output = filedialog.asksaveasfilename(
            title="Build LSOS package",
            defaultextension=".lsos",
            filetypes=[
                ("LSOS Package", "*.lsos")
            ],
            initialfile=(
                self.name.get()
                .strip()
                .replace(" ", "-")
                + ".lsos"
            )
        )

        if not output:
            return

        output_path = Path(output).resolve()

        try:
            if output_path.exists():
                output_path.unlink()

            with zipfile.ZipFile(
                output_path,
                mode="w",
                compression=zipfile.ZIP_DEFLATED,
                compresslevel=9
            ) as archive:

                for file in self.project_dir.rglob("*"):
                    if not file.is_file():
                        continue

                    resolved_file = file.resolve()

                    # Never package the LSOS output into itself.
                    if resolved_file == output_path:
                        continue

                    # Never include private keys.
                    if file.suffix.lower() == ".key":
                        continue

                    # Never include another built LSOS package.
                    if file.suffix.lower() == ".lsos":
                        continue

                    relative_path = file.relative_to(
                        self.project_dir
                    )

                    archive.write(
                        file,
                        arcname=str(relative_path)
                    )

            if not zipfile.is_zipfile(output_path):
                raise RuntimeError(
                    "Generated package is not a valid ZIP/LSOS container."
                )

            with zipfile.ZipFile(
                output_path,
                "r"
            ) as archive:

                required = {
                    "LSOS.MAGIC",
                    "version.txt",
                    "build.prop",
                    "signature.lsing"
                }

                names = set(
                    archive.namelist()
                )

                missing = required - names

                if missing:
                    raise RuntimeError(
                        "Missing required files: "
                        + ", ".join(sorted(missing))
                    )

                broken = archive.testzip()

                if broken is not None:
                    raise RuntimeError(
                        f"Corrupted file inside package: {broken}"
                    )

            digest = sha256_file(
                output_path
            )

            size = output_path.stat().st_size

            self.status.config(
                text=f"Built: {output_path.name}"
            )

            messagebox.showinfo(
                "LSOS Build Complete",
                f"""Build successful.

File:
{output_path}

Size:
{size} bytes

SHA-256:
{digest}
"""
            )

        except Exception as exc:
            if output_path.exists():
                try:
                    output_path.unlink()
                except OSError:
                    pass

            messagebox.showerror(
                "Build Failed",
                str(exc)
            )


def main():
    root = tk.Tk()
    LSOSBuilder(root)
    root.mainloop()


if __name__ == "__main__":
    main()