#!/usr/bin/env python3
"""
Voice assistant over a real cellular phone call.

A Raspberry Pi with a SIM7600 4G USB modem dials a phone number and holds a spoken
conversation: speech-to-text, a language model, text-to-speech, all streaming.

Two modes:
  - Standalone: general voice assistant
  - Triggered (--triggered): task collector mode, writes the tasks it heard to a JSON file
    for another program to act on after the call

Pipeline (all streaming):
  Deepgram WebSocket STT -> Claude streaming -> ElevenLabs streaming TTS -> ffmpeg -> modem PCM

Setup:
  pip install pyserial websockets anthropic        (ffmpeg must be installed too)

  export ANTHROPIC_API_KEY="YOUR_ANTHROPIC_API_KEY"
  export DEEPGRAM_API_KEY="YOUR_DEEPGRAM_API_KEY"
  export ELEVENLABS_API_KEY="YOUR_ELEVENLABS_API_KEY"

  python3 voice_call.py --number +15551234567

Optional environment variables:
  CALL_NUMBER              number to dial when --number is not given
  ASSISTANT_NAME           what the assistant calls itself (default "Assistant")
  USER_NAME                who it is talking to (default "there")
  ASSISTANT_CONTEXT_FILE   text file with background the assistant should know
  GREETING_PCM_FILE        pre-recorded greeting, raw 16-bit mono PCM at the modem's rate
  ELEVENLABS_VOICE_ID      ElevenLabs voice to speak with
  CLAUDE_MODEL             Claude model id
  MODEM_AUDIO_PORT         serial port that carries call audio (default /dev/ttyUSB4)

The script stops ModemManager while it runs (it needs sudo for that) and starts it again
at the end, because ModemManager otherwise holds the modem's serial ports.
"""

import argparse
import asyncio
import json
import os
import struct
import subprocess
import sys
import threading
import time
import urllib.request
from datetime import datetime

import serial
import websockets
from anthropic import Anthropic, APIStatusError, APIConnectionError, APITimeoutError

# Config. Nothing secret lives in this file: keys come from the environment.
AUDIO_PORT = os.environ.get("MODEM_AUDIO_PORT", "/dev/ttyUSB4")
BAUD = 115200
SAMPLE_RATE = 16000   # re-measured at the start of every call, see main()
FRAME_SIZE = 640      # 20 ms of 16-bit mono audio at 16 kHz
PHONE_NUMBER = os.environ.get("CALL_NUMBER", "")   # or pass --number +15551234567

ANTHROPIC_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
DEEPGRAM_KEY = os.environ.get("DEEPGRAM_API_KEY", "")
ELEVENLABS_KEY = os.environ.get("ELEVENLABS_API_KEY", "")
ELEVENLABS_VOICE = os.environ.get("ELEVENLABS_VOICE_ID", "cgSgspJ2msm6clMCkdW9")  # a stock ElevenLabs voice
CLAUDE_MODEL = os.environ.get("CLAUDE_MODEL", "claude-sonnet-4-6")

ASSISTANT_NAME = os.environ.get("ASSISTANT_NAME", "Assistant")
USER_NAME = os.environ.get("USER_NAME", "there")
CONTEXT_FILE = os.environ.get("ASSISTANT_CONTEXT_FILE", "")   # optional background text
GREETING_FILE = os.environ.get("GREETING_PCM_FILE", "")       # optional pre-recorded greeting

call_active = True


# === System Prompt ===

def build_system_prompt(triggered=False, resume=False):
    now = datetime.now()
    date_str = now.strftime("%A, %B %d, %Y")
    time_str = now.strftime("%I:%M %p")

    context = ""
    if CONTEXT_FILE and os.path.exists(CONTEXT_FILE):
        with open(CONTEXT_FILE) as f:
            context = f.read()

    resume_note = ""
    if resume:
        resume_note = (
            "\nIMPORTANT: The previous call was cut off unexpectedly. You are continuing the conversation. "
            "The prior messages are already in your context. Briefly acknowledge the reconnect in your FIRST response, "
            f"then continue from where you left off. Do NOT restart the conversation or ask again for things {USER_NAME} already told you.\n"
        )

    if triggered:
        return f"""You are {ASSISTANT_NAME}, a business operations assistant, on a VOICE CALL with {USER_NAME}.
They asked you to call. They are likely driving or busy.

Date: {date_str}, {time_str} local time.
{resume_note}
YOUR JOB: Collect tasks from {USER_NAME}. You will execute them AFTER the call ends.

RULES:
- Keep every response to ONE short sentence. Max 12 words.
- After each request, confirm briefly: "Got it, [restate task]." then "Anything else?"
- If unclear, ask ONE clarifying question.
- When {USER_NAME} says done/bye, close with a short goodbye.
- Do NOT say you'll "check" or "look up" during the call. Say "I'll handle that after we hang up."
- No markdown, no lists. Talk like a person.

CONTEXT:
{context[:2000] if context else ""}
"""
    else:
        return f"""You are {ASSISTANT_NAME}, a business operations assistant, on a VOICE CALL with {USER_NAME}.

Date: {date_str}, {time_str} local time.

RULES:
- Keep every response to ONE short sentence. Max 12 words.
- Sound warm and direct.
- For data requests, say you will send it in writing after the call.
- No markdown, no lists. Talk like a person.

CONTEXT:
{context[:2000] if context else ""}
"""


# === Modem Helpers ===

def find_at_port():
    for port_num in range(5):
        port = f"/dev/ttyUSB{port_num}"
        try:
            s = serial.Serial(port, BAUD, timeout=0.5)
            s.reset_input_buffer()
            s.write(b"AT\r\n")
            buf = b""
            end = time.time() + 1.5
            while time.time() < end:
                chunk = s.read(256)
                if chunk:
                    buf += chunk
                if b"OK" in buf:
                    s.reset_input_buffer()
                    s.write(b"ATI\r\n")
                    buf2 = b""
                    end2 = time.time() + 1.5
                    while time.time() < end2:
                        chunk = s.read(256)
                        if chunk:
                            buf2 += chunk
                        if b"OK" in buf2:
                            break
                    if b"SIMCOM" in buf2:
                        print(f"  AT port: {port}")
                        return s
            s.close()
        except Exception:
            continue
    return None


def send_at(ser, cmd, wait=1):
    ser.reset_input_buffer()
    ser.write((cmd + "\r\n").encode())
    time.sleep(wait)
    resp = ser.read(ser.in_waiting or 512).decode(errors="ignore").strip()
    print(f"  AT >> {cmd} -> {resp[-40:]}")
    return resp


def make_call(ser):
    print(f"Calling {PHONE_NUMBER}...")
    send_at(ser, f"ATD{PHONE_NUMBER};", wait=1)
    for i in range(25):
        time.sleep(1)
        data = ser.read(ser.in_waiting or 256).decode(errors="ignore").strip()
        if data:
            print(f"  [{i}s] {data}")
        if "VOICE CALL: BEGIN" in data:
            print("Connected!")
            return True
        if "NO CARRIER" in data or "BUSY" in data or "NO ANSWER" in data:
            print("Call failed: " + data)
            return False
    print("No answer after 25s")
    send_at(ser, "ATH")
    return False


def monitor_call(ser):
    global call_active
    while call_active:
        try:
            if ser.in_waiting:
                data = ser.read(ser.in_waiting).decode(errors="ignore")
                if "NO CARRIER" in data or "VOICE CALL: END" in data:
                    print("\n  *** Call ended ***")
                    call_active = False
                    return
        except Exception:
            call_active = False
            return
        time.sleep(0.1)


def play_pcm(audio_port, pcm_data):
    global call_active
    for offset in range(0, len(pcm_data), FRAME_SIZE):
        if not call_active:
            return
        chunk = pcm_data[offset:offset + FRAME_SIZE]
        audio_port.write(chunk)
        if audio_port.in_waiting:
            audio_port.read(audio_port.in_waiting)
        time.sleep(0.02)


# === Streaming Deepgram STT ===

async def deepgram_streaming_stt(audio_port):
    url = (
        f"wss://api.deepgram.com/v1/listen"
        f"?encoding=linear16&sample_rate={SAMPLE_RATE}&channels=1"
        f"&model=nova-2&language=en"
        f"&endpointing=300&utterance_end_ms=1000"
        f"&interim_results=true&vad_events=true"
    )
    headers = {"Authorization": f"Token {DEEPGRAM_KEY}"}

    async with websockets.connect(url, additional_headers=headers) as ws:
        transcript_ready = asyncio.Event()
        final_transcript = [""]

        async def receive_transcripts():
            try:
                async for msg in ws:
                    data = json.loads(msg)
                    if data.get("type") == "Results":
                        speech_final = data.get("speech_final", False)
                        alt = data["channel"]["alternatives"][0]
                        text = alt.get("transcript", "")
                        if speech_final and text.strip():
                            final_transcript[0] = text
                            transcript_ready.set()
                    elif data.get("type") == "UtteranceEnd":
                        if final_transcript[0].strip():
                            transcript_ready.set()
            except websockets.exceptions.ConnectionClosed:
                pass

        receiver = asyncio.create_task(receive_transcripts())

        try:
            while call_active:
                transcript_ready.clear()
                final_transcript[0] = ""

                print("\n  Listening (streaming)...")

                while call_active:
                    if transcript_ready.is_set():
                        break
                    if audio_port.in_waiting:
                        data = audio_port.read(min(audio_port.in_waiting, FRAME_SIZE))
                        if data:
                            await ws.send(data)
                    else:
                        await asyncio.sleep(0.01)

                if not call_active:
                    break

                transcript = final_transcript[0].strip()
                if transcript:
                    yield transcript
        finally:
            receiver.cancel()
            try:
                await ws.send(json.dumps({"type": "CloseStream"}))
            except Exception:
                pass


# === Streaming ElevenLabs TTS ===

def stream_tts_to_modem(text, audio_port):
    global call_active
    t0 = time.time()

    ffmpeg = subprocess.Popen(
        ["ffmpeg", "-y", "-i", "pipe:0",
         "-ar", str(SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-acodec", "pcm_s16le",
         "pipe:1"],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
    )

    url = f"https://api.elevenlabs.io/v1/text-to-speech/{ELEVENLABS_VOICE}/stream"
    body = json.dumps({
        "text": text,
        "model_id": "eleven_turbo_v2_5",
        "voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
    }).encode()
    req = urllib.request.Request(url, data=body, method="POST")
    req.add_header("xi-api-key", ELEVENLABS_KEY)
    req.add_header("Content-Type", "application/json")
    req.add_header("Accept", "audio/mpeg")

    first_byte_logged = False

    try:
        resp = urllib.request.urlopen(req)

        def feed_ffmpeg():
            try:
                while True:
                    chunk = resp.read(4096)
                    if not chunk:
                        break
                    ffmpeg.stdin.write(chunk)
                ffmpeg.stdin.close()
            except Exception:
                try:
                    ffmpeg.stdin.close()
                except Exception:
                    pass

        feeder = threading.Thread(target=feed_ffmpeg, daemon=True)
        feeder.start()

        while call_active:
            pcm_chunk = ffmpeg.stdout.read(FRAME_SIZE)
            if not pcm_chunk:
                break
            if not first_byte_logged:
                print(f"  First audio: {time.time()-t0:.2f}s after TTS start")
                first_byte_logged = True
            audio_port.write(pcm_chunk)
            if audio_port.in_waiting:
                audio_port.read(audio_port.in_waiting)
            time.sleep(0.02)

        feeder.join(timeout=2)
    except Exception as e:
        print(f"  TTS error: {e}")
    finally:
        try:
            ffmpeg.kill()
        except Exception:
            pass

    print(f"  TTS+Play: {time.time()-t0:.1f}s")


# === Claude with retry on transient errors ===

def claude_stream_with_retry(client, model, max_tokens, system, messages, max_retries=2):
    """
    Stream Claude response. On transient API errors (500, timeout, connection),
    retry up to max_retries times with backoff. Raises on final failure.
    Returns (final_text, stream_succeeded).
    """
    last_err = None
    for attempt in range(max_retries + 1):
        try:
            with client.messages.stream(
                model=model,
                max_tokens=max_tokens,
                system=system,
                messages=messages,
            ) as stream:
                collected = ""
                for token in stream.text_stream:
                    collected += token
            return collected, True
        except (APIStatusError, APIConnectionError, APITimeoutError) as e:
            last_err = e
            # Only retry on 5xx or connection errors, not 4xx
            if isinstance(e, APIStatusError) and 400 <= e.status_code < 500:
                raise
            wait = 1 + attempt  # 1s, 2s, 3s
            print(f"  Claude API error (attempt {attempt+1}/{max_retries+1}): {type(e).__name__}: {str(e)[:200]}")
            if attempt < max_retries:
                print(f"  Retrying in {wait}s...")
                time.sleep(wait)
        except Exception as e:
            # Unknown error - don't retry
            raise
    # Exhausted retries
    raise last_err if last_err else RuntimeError("Claude stream failed with no error")


# === Task Extraction ===

def extract_tasks(messages, client):
    transcript_lines = []
    for msg in messages:
        role = USER_NAME if msg["role"] == "user" else ASSISTANT_NAME
        transcript_lines.append(f"{role}: {msg['content']}")
    transcript = "\n".join(transcript_lines)

    response = client.messages.create(
        model=CLAUDE_MODEL,
        max_tokens=1024,
        system="Extract actionable tasks from this voice call transcript. Return ONLY valid JSON, no markdown.",
        messages=[{
            "role": "user",
            "content": f"""Extract all tasks/requests the caller made during this call.

Transcript:
{transcript}

Return JSON:
{{"tasks": ["task 1 description", "task 2 description", ...], "transcript": "full transcript"}}

If no actionable tasks, return {{"tasks": [], "transcript": "full transcript"}}"""
        }]
    )

    try:
        text = response.content[0].text.strip()
        if text.startswith("```"):
            text = text.split("\n", 1)[1].rsplit("```", 1)[0].strip()
        return json.loads(text)
    except (json.JSONDecodeError, IndexError):
        return {"tasks": [], "transcript": transcript, "parse_error": True}


# === Conversation Loop ===

async def conversation_loop(audio_port, ser, system_prompt, triggered, messages_ref):
    """
    messages_ref: mutable list, mutated in-place as conversation progresses.
    This ensures main() has access to partial state if we crash.
    """
    global call_active

    client = Anthropic(api_key=ANTHROPIC_KEY)
    messages = messages_ref  # use the same list object
    is_resume = len(messages) > 0

    # Greeting depends on mode
    print("\n--- Greeting ---")
    if is_resume:
        # Resume: TTS a short "we got cut off" message (no pre-recorded for this)
        greeting_text = f"Hey {USER_NAME}, sorry we got cut off. Where were we?"
        stream_tts_to_modem(greeting_text, audio_port)
    else:
        # The pre-recorded greeting must already be raw PCM at the detected SAMPLE_RATE.
        if triggered and GREETING_FILE and os.path.exists(GREETING_FILE):
            with open(GREETING_FILE, "rb") as f:
                greeting_pcm = f.read()
            t0 = time.time()
            play_pcm(audio_port, greeting_pcm)
            print(f"  Pre-recorded greeting: {time.time()-t0:.1f}s")
            greeting_text = f"Hey {USER_NAME}, you asked me to call. What do you need?"
        else:
            greeting_text = (f"Hey {USER_NAME}, you asked me to call. What do you need?" if triggered
                             else f"Hi {USER_NAME}! It's {ASSISTANT_NAME}. What's on your mind?")
            stream_tts_to_modem(greeting_text, audio_port)
    messages.append({"role": "assistant", "content": greeting_text})

    # Streaming conversation
    async for transcript in deepgram_streaming_stt(audio_port):
        if not call_active:
            break

        t0 = time.time()
        print(f'  {USER_NAME}: "{transcript}"')

        lower = transcript.lower()
        if any(w in lower for w in ["bye", "goodbye", "hang up", "end call", "that's it", "that's all"]):
            if triggered and messages:
                messages.append({"role": "user", "content": transcript})
                with client.messages.stream(
                    model=CLAUDE_MODEL, max_tokens=150,
                    system=system_prompt, messages=messages,
                ) as stream:
                    farewell = ""
                    for token in stream.text_stream:
                        farewell += token
                messages.append({"role": "assistant", "content": farewell})
                print(f'  {ASSISTANT_NAME}: "{farewell}"')
                stream_tts_to_modem(farewell, audio_port)
            else:
                stream_tts_to_modem(f"Bye {USER_NAME}! Talk soon.", audio_port)
            break

        messages.append({"role": "user", "content": transcript})

        t_claude = time.time()
        collected = ""
        pending_sentence = ""

        # Try streaming with retry on transient API errors
        try:
            with client.messages.stream(
                model=CLAUDE_MODEL,
                max_tokens=150,
                system=system_prompt,
                messages=messages,
            ) as stream:
                for token in stream.text_stream:
                    collected += token
                    pending_sentence += token

                    for end_char in [".", "!", "?"]:
                        if end_char in pending_sentence:
                            parts = pending_sentence.split(end_char, 1)
                            sentence = parts[0] + end_char
                            pending_sentence = parts[1] if len(parts) > 1 else ""

                            if sentence.strip():
                                t_first = time.time()
                                print(f'  {ASSISTANT_NAME}: "{sentence.strip()}" (Claude: {t_first-t_claude:.1f}s, Total: {t_first-t0:.1f}s)')
                                stream_tts_to_modem(sentence.strip(), audio_port)
                            break
        except (APIStatusError, APIConnectionError, APITimeoutError) as e:
            # Transient error mid-stream. Try non-streaming retry as fallback.
            print(f"  Claude streaming failed: {type(e).__name__}: {str(e)[:200]}")
            print("  Retrying with non-streaming call...")
            try:
                collected2, _ = claude_stream_with_retry(
                    client, CLAUDE_MODEL, 150, system_prompt, messages, max_retries=2
                )
                # Speak whatever we got (prefer the retry result if streaming gave nothing)
                if not collected.strip() and collected2.strip():
                    collected = collected2
                    pending_sentence = collected2
                    # Drain pending_sentence below
            except Exception as e2:
                print(f"  Claude retry also failed: {e2}")
                # Inform user verbally if we have no answer yet
                if not collected.strip():
                    try:
                        stream_tts_to_modem("Sorry, I hit a glitch. Can you say that again?", audio_port)
                    except Exception:
                        pass
                    # Do NOT raise - keep the call alive and try the next turn
                    continue

        if pending_sentence.strip():
            print(f'  {ASSISTANT_NAME}: "{pending_sentence.strip()}"')
            stream_tts_to_modem(pending_sentence.strip(), audio_port)

        messages.append({"role": "assistant", "content": collected})
        print(f"  Total turn: {time.time()-t0:.1f}s")


# === Main ===

def write_task_file(path, status, messages, tasks=None, error=None, retry_count=0):
    """Crash-safe task file writer. Always includes messages for possible resume."""
    if not path:
        return
    data = {
        "status": status,  # "completed" | "crashed" | "error" | "no_answer"
        "timestamp": datetime.now().isoformat(),
        "messages": messages or [],
        "tasks": tasks or [],
        "retry_count": retry_count,
    }
    if error:
        data["error"] = str(error)
    try:
        with open(path, "w") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        try:
            os.chmod(path, 0o666)
        except Exception:
            pass
    except Exception as e:
        print(f"WARNING: Could not write task file {path}: {e}")


def main():
    global call_active, SAMPLE_RATE, FRAME_SIZE, PHONE_NUMBER

    parser = argparse.ArgumentParser()
    parser.add_argument("--number", default=None,
                        help="Phone number to dial, international format, e.g. +15551234567")
    parser.add_argument("--triggered", action="store_true",
                        help="Task-collector mode (started by another program)")
    parser.add_argument("--task-file", default=None,
                        help="Path to write task JSON output")
    parser.add_argument("--context-file", default=None,
                        help="Path to previous task file to resume conversation from")
    parser.add_argument("--retry-count", type=int, default=0,
                        help="Current retry attempt (for resume tracking)")
    args = parser.parse_args()

    PHONE_NUMBER = args.number or PHONE_NUMBER
    if not PHONE_NUMBER:
        sys.exit("No number to dial. Pass --number +15551234567 or set CALL_NUMBER.")
    missing = [name for name, value in (("ANTHROPIC_API_KEY", ANTHROPIC_KEY),
                                        ("DEEPGRAM_API_KEY", DEEPGRAM_KEY),
                                        ("ELEVENLABS_API_KEY", ELEVENLABS_KEY)) if not value]
    if missing:
        sys.exit("Missing environment variables: " + ", ".join(missing))

    # Load prior conversation if resuming
    prior_messages = None
    if args.context_file and os.path.exists(args.context_file):
        try:
            with open(args.context_file) as f:
                prior = json.load(f)
            prior_messages = prior.get("messages") or []
            print(f"Resuming from {args.context_file}: {len(prior_messages)} prior messages")
        except Exception as e:
            print(f"WARNING: Could not load context file: {e}")

    call_active = True
    print("Stopping ModemManager...")
    subprocess.run(["sudo", "systemctl", "stop", "ModemManager"], capture_output=True)
    time.sleep(1)

    print("Finding AT port...")
    ser = find_at_port()
    if not ser:
        print("ERROR: No AT port found")
        write_task_file(args.task_file, "error", prior_messages,
                        error="No modem found", retry_count=args.retry_count)
        return

    send_at(ser, "AT+CPCMREG=0")
    send_at(ser, "ATH")
    time.sleep(2)

    if not make_call(ser):
        ser.close()
        subprocess.run(["sudo", "systemctl", "start", "ModemManager"], capture_output=True)
        write_task_file(args.task_file, "no_answer", prior_messages,
                        error="Call failed - no answer or busy", retry_count=args.retry_count)
        return

    time.sleep(1)
    send_at(ser, "AT+CPCMREG=1")
    time.sleep(0.5)

    audio = serial.Serial(AUDIO_PORT, BAUD, timeout=1)
    audio.reset_input_buffer()

    # Quick sample rate detection (0.5s)
    count = 0
    t0 = time.time()
    while time.time() - t0 < 0.5:
        if audio.in_waiting:
            count += len(audio.read(audio.in_waiting))
        else:
            time.sleep(0.005)
    bps = count / 0.5
    detected = int(bps / 2)
    if abs(detected - 8000) < 2000:
        SAMPLE_RATE = 8000
        FRAME_SIZE = 320
    else:
        SAMPLE_RATE = 16000
        FRAME_SIZE = 640
    print(f"  PCM: {detected} Hz -> using {SAMPLE_RATE}")

    system_prompt = build_system_prompt(triggered=args.triggered, resume=bool(prior_messages))

    monitor = threading.Thread(target=monitor_call, args=(ser,), daemon=True)
    monitor.start()

    messages = list(prior_messages) if prior_messages else []
    crashed = False
    crash_error = None
    try:
        asyncio.run(conversation_loop(
            audio, ser, system_prompt, args.triggered,
            messages_ref=messages,
        ))
    except KeyboardInterrupt:
        print("\nInterrupted")
        crashed = True
        crash_error = "KeyboardInterrupt"
    except Exception as e:
        print(f"\nCRASH in conversation loop: {type(e).__name__}: {e}")
        crashed = True
        crash_error = f"{type(e).__name__}: {e}"
    finally:
        call_active = False
        print("\nCleaning up...")
        try:
            send_at(ser, "AT+CPCMREG=0")
        except Exception:
            pass
        try:
            audio.close()
        except Exception:
            pass
        try:
            send_at(ser, "ATH")
        except Exception:
            pass
        try:
            ser.close()
        except Exception:
            pass
        subprocess.run(["sudo", "systemctl", "start", "ModemManager"], capture_output=True)

    # Always write a task file (crash-safe)
    if args.task_file:
        if crashed:
            print("\nCall crashed - saving context for possible resume")
            write_task_file(args.task_file, "crashed", messages,
                            error=crash_error, retry_count=args.retry_count)
        elif messages:
            print("\nExtracting tasks...")
            try:
                client = Anthropic(api_key=ANTHROPIC_KEY)
                task_data = extract_tasks(messages, client)
                write_task_file(args.task_file, "completed", messages,
                                tasks=task_data.get("tasks", []),
                                retry_count=args.retry_count)
                print(f"Tasks: {task_data.get('tasks', [])}")
            except Exception as e:
                print(f"Task extraction failed: {e}")
                write_task_file(args.task_file, "completed", messages,
                                error=f"task_extraction_failed: {e}",
                                retry_count=args.retry_count)
        else:
            write_task_file(args.task_file, "completed", [],
                            retry_count=args.retry_count)

    print("Done.")


if __name__ == "__main__":
    main()
