"""
Copyright (C) 2024-2026 Mo Zhou <lumin@debian.org>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""
import os
import shutil
import sys

import tomllib  # requires python >= 3.10
from rich.console import Console
from rich.panel import Panel
from rich.traceback import install

install()

# before we print anything, even before initializing class instances,
# we need to detect a special mode that does not allow any printing
# in stderr
if "debgpt" in sys.argv[0] and "pipe" in sys.argv:
    devnull = os.open(os.devnull, os.O_WRONLY)
    # Redirect stderr to /dev/null
    os.dup2(devnull, 2)

# default console that writes to stderr
console = Console(stderr=True)

########################
# Configuration handling
########################

# The legacy (pre-0.9) layout kept everything under ~/.debgpt . We now
# follow the XDG Base Directory Specification: configuration goes to
# ~/.config/debgpt/ and cache (sqlite cache + session json logs) goes to
# ~/.cache/debgpt/ . The XDG_CONFIG_HOME / XDG_CACHE_HOME environment
# variables are honored when set.
LEGACY_HOME = os.path.expanduser("~/.debgpt")
XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
XDG_CACHE_HOME = os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache")

CONFIG_HOME = os.path.join(XDG_CONFIG_HOME, "debgpt")
CACHE_HOME = os.path.join(XDG_CACHE_HOME, "debgpt")

# HOME is kept as an alias for backward compatibility of the variable name.
# It now points to the cache directory, where the sqlite cache and the
# session json logs live.
HOME = CACHE_HOME
CONFIG = os.path.join(CONFIG_HOME, "config.toml")
CACHE = os.path.join(CACHE_HOME, "cache.sqlite")


def migrate_legacy_home() -> None:
    """
    Migrate the legacy ~/.debgpt layout to the XDG-style directories:

      ~/.debgpt/config.toml   ->  ~/.config/debgpt/config.toml
      ~/.debgpt/cache.sqlite  ->  ~/.cache/debgpt/cache.sqlite
      ~/.debgpt/*.json        ->  ~/.cache/debgpt/*.json   (session logs)

    Only the files we recognize are moved. If any unexpected files remain in
    ~/.debgpt, the directory is kept intact (along with those files) and the
    user is notified. Otherwise the now-empty legacy directory is removed.
    The user is given a clear notice whenever anything is migrated.
    """
    if not os.path.isdir(LEGACY_HOME):
        return

    # classify every entry in the legacy directory
    migrations = []  # list of (src, dst) we know how to migrate
    leftovers = []  # entries we do not touch
    for name in sorted(os.listdir(LEGACY_HOME)):
        src = os.path.join(LEGACY_HOME, name)
        if name == "config.toml" and os.path.isfile(src):
            migrations.append((src, CONFIG))
        elif name == "cache.sqlite" and os.path.isfile(src):
            migrations.append((src, CACHE))
        elif name.endswith(".json") and os.path.isfile(src):
            migrations.append((src, os.path.join(CACHE_HOME, name)))
        else:
            leftovers.append(src)

    if not migrations:
        # nothing recognizable to migrate. If the directory is completely
        # empty, silently remove it; otherwise leave it (and its files) alone.
        if not leftovers:
            try:
                os.rmdir(LEGACY_HOME)
            except OSError:
                pass
        return

    # perform the migration
    os.makedirs(CONFIG_HOME, exist_ok=True)
    os.makedirs(CACHE_HOME, exist_ok=True)
    moved = []
    for src, dst in migrations:
        if os.path.exists(dst):
            # never overwrite a file that already exists in the new location
            leftovers.append(src)
            continue
        os.makedirs(os.path.dirname(dst), exist_ok=True)
        shutil.move(src, dst)
        moved.append((src, dst))

    # remove the legacy directory only when it is now empty
    removed_dir = False
    if not leftovers:
        try:
            os.rmdir(LEGACY_HOME)
            removed_dir = True
        except OSError:
            pass

    # give the user a clear notice
    lines = [
        f"DebGPT migrated its files out of the legacy directory "
        f"[cyan]{LEGACY_HOME}[/cyan] into the standard locations:",
        "",
    ]
    for src, dst in moved:
        lines.append(
            f"  [green]moved[/green] {os.path.basename(src)} " f"-> [cyan]{dst}[/cyan]"
        )
    if leftovers:
        lines.append("")
        lines.append(
            f"The following unexpected item(s) were left untouched "
            f"in [cyan]{LEGACY_HOME}[/cyan]:"
        )
        for src in sorted(leftovers):
            lines.append(f"  [yellow]kept[/yellow]  {os.path.basename(src)}")
    elif removed_dir:
        lines.append("")
        lines.append(
            f"The empty legacy directory [cyan]{LEGACY_HOME}[/cyan] " f"was removed."
        )
    console.print(
        Panel(
            "\n".join(lines), title="DebGPT Directory Migration", border_style="yellow"
        )
    )


# run the migration as early as possible, before any configuration is loaded
migrate_legacy_home()


class Config(object):
    def __init__(self, config: str = CONFIG, verbose: bool = False):
        # The built-in defaults will be overridden by config file
        self.toml = {
            # CLI/Frontend Bebavior
            "frontend": "openai",
            "debgpt_home": CACHE_HOME,
            "color": "auto",
            "render": "auto",
            "vertical_overflow": "visible",  # 'visible' or 'ellipsis'
            # LLM Inference Parameters
            "temperature": 0.2,
            "top_p": 1.0,
            "context_window_size": 1000000,
            # Mapreduce Settings
            "mapreduce_chunksize": 65536,
            "mapreduce_parallelism": 8,
            # OpenAI Frontend Specific
            "openai_base_url": "https://api.openai.com/v1",
            "openai_model": "gpt-4o",
            "openai_api_key": "your-openai-api-key",
            # Anthropic Frontend Specific
            "anthropic_base_url": "https://api.anthropic.com",
            "anthropic_api_key": "your-anthropic-api-key",
            "anthropic_model": "claude-3-5-sonnet-20241022",
            # Gemini Frontend Specific
            "google_api_key": "your-google-api-key",
            "google_model": "gemini-1.5-flash",
            # DeepSeek Frontend Specific
            "deepseek_base_url": "https://api.deepseek.com",
            "deepseek_model": "deepseek-reasoner",
            "deepseek_api_key": "your-deepseek-api-key",
            # vLLM (or any other OpenAI-API compatible service) Specific
            "vllm_base_url": "http://localhost:8000/v1",
            "vllm_api_key": "your-vllm-api-key",
            "vllm_model": "NousResearch/Meta-Llama-3-8B-Instruct",
            # ZMQ Frontend Specific
            "zmq_backend": "tcp://localhost:11177",
            # System messages
            "system_message": """\
You are an excellent free software developer. You write high-quality code.
You aim to provide people with professional and accurate information.
You cherish software freedom. You obey the Debian Social Contract and the
Debian Free Software Guidelines. You follow the Debian Policy. You must
always cite resources in your response when applicable, and provide the
URL links in plain text format in the response.

`sbuild` is the standard tool for building Debian packages. The Debian Policy
Manual [policy] dictates the official guidelines and technical requirements for
packages, while `lintian` is the automated tool used to verify compliance with
this policy. The Debian Developer's Reference [devref] serves as the general
guide for Debian packaging best practices and procedures. The Guide for Debian
Maintainers [debmake-doc] is a good introduction for entry level Debian contributors.
Feel free to fetch these documentations when in need for your reference.

References:
[policy] https://www.debian.org/doc/debian-policy/
[devref] https://www.debian.org/doc/manuals/developers-reference/
[debmake-doc] https://www.debian.org/doc/manuals/debmake-doc/index.en.html
For more resources see: https://www.debian.org/doc/
""",
        }
        # the built-in defaults will be overridden by config file.
        # ensure both the config and cache directories exist.
        for directory in (CONFIG_HOME, CACHE_HOME):
            if not os.path.exists(directory):
                if verbose:
                    console.log(f"Creating directory {directory}")
                os.makedirs(directory, exist_ok=True)
        if os.path.exists(config):
            if verbose:
                console.log(f"Loading configuration from {config}")
            with open(config, "rb") as f:
                content = tomllib.load(f)
                self.toml.update(content)
        # some arguments will be overrden by environment variables
        if (openai_api_key := os.getenv("OPENAI_API_KEY", None)) is not None:
            if verbose:
                console.log("Found environment variable OPENAI_API_KEY.")
            self.toml["openai_api_key"] = openai_api_key
        if (anthropic_api_key := os.getenv("ANTHROPIC_API_KEY", None)) is not None:
            if verbose:
                console.log("Found environment variable ANTHROPIC_API_KEY.")
            self.toml["anthropic_api_key"] = anthropic_api_key
        if (google_api_key := os.getenv("GOOGLE_API_KEY", None)) is not None:
            if verbose:
                console.log("Found environment variable GOOGLE_API_KEY.")
            self.toml["google_api_key"] = google_api_key
        # all the above will be overridden by command line arguments

    def __getitem__(self, index):
        return self.toml.__getitem__(index)

    def __getattr__(self, index):
        return self.toml.__getitem__(index)
