from __future__ import annotations

import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Sequence

import black
import black.mode
import isort
from codegen.models import Program
from loguru import logger
from sera.misc import File, Formatter
from sera.typing import Language


@dataclass
class Package:
    """Represent an target generated module in the application"""

    app: App
    path: str
    dir: Path
    language: Language

    def ensure_exists(self):
        """Ensure the module exists"""
        self.dir.mkdir(parents=True, exist_ok=True)
        if self.language == Language.Python:
            if not (self.dir / "__init__.py").exists():
                (self.dir / "__init__.py").touch()

    def pkg(self, name: str) -> Package:
        """Create a package in this package"""
        return Package(self.app, f"{self.path}.{name}", self.dir / name, self.language)

    def module(self, name: str) -> Module:
        """Create a module in this package"""
        return Module(self, name, self.language)


@dataclass
class Module:
    """Represent a module in a package"""

    package: Package
    name: str
    language: Language

    @property
    def path(self) -> str:
        return f"{self.package.path}.{self.name}"

    def write(self, program: Program):
        """Write the module to disk"""
        self.package.ensure_exists()
        if self.language == Language.Python:
            try:
                code = black.format_str(
                    program.root.to_python(),
                    mode=black.Mode(
                        target_versions={black.mode.TargetVersion.PY312},
                    ),
                )
                code = isort.code(code, profile="black")
            except:
                logger.error("Error writing module {}", self.path)
                print(">>> Program")
                print(program.root.to_python())
                print("<<<")
                raise
        else:
            assert self.language == Language.Typescript
            try:
                code = program.root.to_typescript()
            except:
                logger.error("Error writing module {}", self.path)
                print(">>> Program")
                print(program.root.to_typescript())
                print("<<<")
                raise

        if self.language == Language.Python:
            outfile = self.package.dir / f"{self.name}.py"
            copyright_statement = f"# Generated by SERA. All rights reserved.\n\n"
        else:
            assert self.language == Language.Typescript
            outfile = self.package.dir / f"{self.name}.ts"
            copyright_statement = f"/// Generated by SERA. All rights reserved.\n\n"

        if outfile.exists():
            outfile_content = outfile.read_text()
            if outfile_content.startswith("# sera:skip") or outfile_content.startswith(
                "/// sera:skip"
            ):
                logger.info(
                    "`{}` already exists and is in manual edit mode. Skip updating it.",
                    outfile,
                )
                return

        outfile.write_text(copyright_statement + code)

        if self.language == Language.Typescript:
            Formatter.get_instance().register(
                File(path=outfile, language=self.language)
            )

    def exists(self) -> bool:
        """Check if the module exists"""
        if self.language == Language.Python:
            return (self.package.dir / f"{self.name}.py").exists()
        else:
            return (self.package.dir / f"{self.name}.ts").exists()


@dataclass
class AppModels(Package):
    data: Package
    db: Package

    @staticmethod
    def from_pkg(pkg: Package) -> AppModels:
        """Create an AppModels from a package"""
        return AppModels(
            pkg.app,
            path=pkg.path,
            dir=pkg.dir,
            language=pkg.language,
            data=pkg.pkg("data"),
            db=pkg.pkg("db"),
        )


@dataclass
class App:
    """Represent the generated application"""

    # top-level package of the application
    root: Package

    # application configuration
    config: Module

    # models of the application
    models: AppModels

    # services of the application, which encode the business logic
    services: Package

    # API of the application
    api: Package

    schema_files: Sequence[Path]

    language: Language

    def __init__(
        self, name: str, dir: Path, schema_files: Sequence[Path], language: Language
    ):
        self.root = Package(self, name, dir, language)

        self.config = self.root.module("config")
        self.models = AppModels.from_pkg(self.root.pkg("models"))
        self.services = self.root.pkg("services")
        self.api = self.root.pkg("api")

        self.schema_files = schema_files

        self.language = language

    @property
    def name(self) -> str:
        """Get the name of the application"""
        return self.root.dir.name
