#!/usr/bin/env python3
"""
todo.py

A minimalist CLI todo application with priorities, tags, sorting, JIRA import,
and completion-date tracking. Uses INI config at ~/.config/todo/config.ini.
"""

import sys
import os
import argparse
import re
import configparser
import urllib.request
import urllib.parse
import json

from dataclasses import dataclass
from datetime import datetime, date
from pathlib import Path
from typing import List, Optional

from rich.console import Console
from rich.table import Table
from rich.prompt import Prompt, IntPrompt
from rich.text import Text

# -------------------------------------------------------------------
# Configuration loading via configparser
# -------------------------------------------------------------------

DEFAULT_CONFIG = {
    'todo': {
        'todo_file': '~/sync/todo/todo.txt',
        'archive_file': '~/sync/todo/todo-archive.txt',
        'default_priority': 'B'
    },
    'tag_colors': {
        '@jira': 'blue',
        '@me':   'magenta',
        '@work': 'green'
    },
    'priority_labels': {
        'A': 'High',
        'B': 'Med',
        'C': 'Low'
    },
    'priority_styles': {
        'A': 'bold red',
        'B': 'yellow',
        'C': 'green'
    }
}

def load_config():
    cfg = configparser.ConfigParser()
    cfg.optionxform = str
    config_path = Path.home() / '.config' / 'todo' / 'config.ini'
    # Initialize defaults
    for section, opts in DEFAULT_CONFIG.items():
        if not cfg.has_section(section):
            cfg[section] = {}
        for k, v in opts.items():
            cfg[section].setdefault(k, str(v))
    # Read user overrides
    if config_path.exists():
        cfg.read(config_path)
    return {
        'todo_file':        Path(cfg['todo']['todo_file']).expanduser(),
        'archive_file':     Path(cfg['todo']['archive_file']).expanduser(),
        'default_priority': cfg['todo']['default_priority'],
        'tag_colors':       dict(cfg['tag_colors']),
        'priority_labels':  dict(cfg['priority_labels']),
        'priority_styles':  dict(cfg['priority_styles']),
    }

CFG = load_config()
TODO_FILE_PATH    = CFG['todo_file']
ARCHIVE_FILE_PATH = CFG['archive_file']
DEFAULT_PRIORITY  = CFG['default_priority']
TAG_COLOR_MAP     = CFG['tag_colors']
PRIORITY_LABELS   = CFG['priority_labels']
PRIORITY_STYLE_MAP= CFG['priority_styles']
DATE_FORMAT       = '%Y-%m-%d'

console = Console()

# -------------------------------------------------------------------
# Task class with completion-date support
# -------------------------------------------------------------------

LINE_RE = re.compile(
    r'^(?:x \((?P<completed>\d{4}-\d{2}-\d{2})\)\s*)?'  # optional done with date
    r'(?:\[#(?P<priority>[ABC])\]\s*)?'                   # optional priority
    r'\((?P<created>\d{4}-\d{2}-\d{2})\)\s*'           # created date
    r'(?P<description>.+)$'                                 # description+tags
)

@dataclass
class Task:
    completed: Optional[date]
    priority: str
    created: date
    description: str
    tags: List[str]

    @property
    def done(self) -> bool:
        return self.completed is not None

    @classmethod
    def from_line(cls, line: str) -> Optional['Task']:
        m = LINE_RE.match(line)
        if not m:
            return None
        gd = m.groupdict()
        # Parse dates
        completed = None
        if gd.get('completed'):
            completed = datetime.strptime(gd['completed'], DATE_FORMAT).date()
        prio = gd['priority'] or DEFAULT_PRIORITY
        created = datetime.strptime(gd['created'], DATE_FORMAT).date()
        parts = gd['description'].split()
        tags = [w for w in parts if w.startswith('@')]
        desc = ' '.join(w for w in parts if not w.startswith('@'))
        return cls(completed=completed,
                   priority=prio,
                   created=created,
                   description=desc,
                   tags=tags)

    def to_line(self) -> str:
        parts = []
        if self.done:
            parts.append(f"x ({self.completed.strftime(DATE_FORMAT)}) ")
        parts.append(f"[#%s] " % self.priority)
        parts.append(f"({self.created.strftime(DATE_FORMAT)}) ")
        parts.append(self.description)
        if self.tags:
            parts.append(' ' + ' '.join(self.tags))
        return ''.join(parts)

    def mark_done(self) -> 'Task':
        return Task(completed=date.today(),
                    priority=self.priority,
                    created=self.created,
                    description=self.description,
                    tags=self.tags)

# -------------------------------------------------------------------
# I/O
# -------------------------------------------------------------------

def read_tasks() -> List[Task]:
    if not TODO_FILE_PATH.exists():
        return []
    lines = TODO_FILE_PATH.read_text().splitlines()
    return [t for t in (Task.from_line(l) for l in lines) if t]


def write_tasks(tasks: List[Task]):
    TODO_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
    lines = [t.to_line() for t in tasks]
    TODO_FILE_PATH.write_text("\n".join(lines) + "\n")

# -------------------------------------------------------------------
# Display & Sorting
# -------------------------------------------------------------------

def display_tasks(tasks: List[Task], show_all: bool, sort_by: Optional[str]):
    # filter
    display = tasks if show_all else [t for t in tasks if not t.done]
    # sort
    if sort_by == 'created':
        display.sort(key=lambda t: t.created)
    elif sort_by == 'priority':
        display.sort(key=lambda t: t.priority)
    elif sort_by and sort_by.startswith('tag:'):
        tag = sort_by.split(':',1)[1]
        display.sort(key=lambda t: tag in t.tags, reverse=True)

    # columns order
    if show_all:
        cols = ['#','Created','Completed','Pri','Done','Description','Tags']
    else:
        cols = ['#','Created','Pri','Description','Tags']

    table = Table(title='TODOs (all)' if show_all else 'TODOs')
    for c in cols:
        table.add_column(c, justify='right' if c=='#' else None)

    for idx, t in enumerate(display, start=1):
        pri_lbl = PRIORITY_LABELS.get(t.priority, t.priority)
        pri_cell= Text(pri_lbl, style=PRIORITY_STYLE_MAP.get(t.priority,''))
        tag_cell= Text()
        for tag in t.tags:
            tag_cell.append(tag, TAG_COLOR_MAP.get(tag,''))
            tag_cell.append(' ')
        # cells
        row = [str(idx),
               t.created.strftime(DATE_FORMAT)]
        if show_all:
            comp = t.completed.strftime(DATE_FORMAT) if t.completed else ''
            row.append(comp)
        row.append(pri_cell)
        if show_all:
            row.append('✔' if t.done else '')
        row.append(t.description)
        row.append(tag_cell)
        table.add_row(*row)

    console.print(table)

# -------------------------------------------------------------------
# Commands
# -------------------------------------------------------------------

def cmd_ls(args):
    display_tasks(read_tasks(), show_all=False, sort_by=args.sort)

def cmd_lsa(args):
    display_tasks(read_tasks(), show_all=True,  sort_by=args.sort)

def cmd_add(args):
    if args.text:
        entry = ' '.join(args.text)
        if not entry.startswith('[#'):
            console.print('[yellow]No priority; defaulting to Med (B).[/]')
            entry = '[#B] ' + entry
        if '@' not in entry:
            console.print('[yellow]No tag; defaulting to @misc.[/]')
            entry += ' @misc'
        t = Task.from_line(entry)
        tasks = read_tasks() + ([t] if t else [])
        write_tasks(tasks)
        console.print(f'[green]Added:[/] {t.description if t else entry}')
    else:
        desc = Prompt.ask('Task description')
        prio = Prompt.ask('Priority (A=High,B=Med,C=Low)', choices=['A','B','C'], default=DEFAULT_PRIORITY)
        tag  = Prompt.ask('Tag (e.g. @work,@me,@jira)', default='@misc')
        t = Task(completed=None, priority=prio, created=date.today(), description=desc, tags=[tag])
        tasks = read_tasks() + [t]
        write_tasks(tasks)
        console.print(f'[green]Added:[/] {desc}')

def cmd_done(args):
    all_tasks = read_tasks()
    active_idxs = [i for i,t in enumerate(all_tasks) if not t.done]
    try:
        n = int(args.number)
        if not (1 <= n <= len(active_idxs)):
            raise IndexError
        idx = active_idxs[n-1]
        all_tasks[idx] = all_tasks[idx].mark_done()
        write_tasks(all_tasks)
        console.print(f"[green]Completed:[/] {all_tasks[idx].description}")
    except:
        console.print('[red]Invalid task number[/]')

def cmd_donei(args):
    all_tasks = read_tasks()
    active = [(i,t) for i,t in enumerate(all_tasks) if not t.done]
    if not active:
        console.print('[yellow]No active tasks.[/]')
        return
    for disp,i in enumerate(active, start=1):
        console.print(f"{disp}: {i[1].description}")
    choice = IntPrompt.ask('Complete task #')
    cmd_done(argparse.Namespace(number=str(choice)))

def cmd_archive(args):
    tasks = read_tasks()
    done = [t for t in tasks if t.done]
    if not done:
        console.print('[yellow]No tasks to archive[/]')
        return
    ARCHIVE_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
    with ARCHIVE_FILE_PATH.open('a') as f:
        for t in done:
            f.write(t.to_line() + '\n')
    write_tasks([t for t in tasks if not t.done])
    console.print(f'[green]Archived {len(done)}[/]')

def cmd_tags(args):
    tasks = [t for t in read_tasks() if args.tag in t.tags and not t.done]
    if not tasks:
        console.print(f'[yellow]No active tasks with tag {args.tag}[/]')
    else:
        display_tasks(tasks, show_all=False, sort_by=None)

def cmd_edit(args):
    editor = os.getenv('EDITOR','nano')
    os.execvp(editor, [editor, str(TODO_FILE_PATH)])

def cmd_jira(args):
    console.print('[italic]JIRA import not yet implemented[/]')

# -------------------------------------------------------------------
# CLI dispatch
# -------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(prog='todo')
    sub = parser.add_subparsers(dest='cmd', required=True)

    p_ls  = sub.add_parser('ls', help='List active tasks')
    p_ls.add_argument('--sort', choices=['created','priority','tag:@jira','tag:@work','tag:@me'])

    p_lsa = sub.add_parser('lsa', help='List all tasks')
    p_lsa.add_argument('--sort', choices=p_ls.get_default('sort') and p_ls.choices or None)

    sub.add_parser('archive', help='Archive completed tasks')

    p_add = sub.add_parser('add', help='Add a task')
    p_add.add_argument('text', nargs=argparse.REMAINDER)

    p_done = sub.add_parser('done', help='Mark a task done')
    p_done.add_argument('number')

    sub.add_parser('donei', help='Interactive completion')

    p_tags = sub.add_parser('tags', help='Filter by tag')
    p_tags.add_argument('tag')

    sub.add_parser('edit', help='Open todo file in editor')

    p_jira = sub.add_parser('jira', help='Import JIRA issues')
    p_jira.add_argument('--project', required=True)
    p_jira.add_argument('--status','-s',action='append',default=['Open','In Progress'])
    p_jira.add_argument('--assignee','-a',default='currentUser()')
    p_jira.add_argument('--board','-b',type=int)

    args = parser.parse_args()
    {
        'ls':      cmd_ls,
        'lsa':     cmd_lsa,
        'add':     cmd_add,
        'done':    cmd_done,
        'donei':   cmd_donei,
        'archive': cmd_archive,
        'tags':    cmd_tags,
        'edit':    cmd_edit,
        'jira':    cmd_jira,
    }[args.cmd](args)

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        console.print('\n[red]^C Aborted[/]')
