from __future__ import absolute_import, with_statement, print_function, division, unicode_literals
from .commandenv import ResultRow
from .exceptions import CommandLineError
from .parsing import *
from ..tradedb import AmbiguityError
from ..tradedb import System, Station
from ..tradedb import TradeDB
from .. import utils
from ..formatting import max_len

from .. import cache
from .. import csvexport
import difflib
import re
import sys

######################################################################
# Parser config

help='Add (or update) a station entry'
name='station'
epilog=None
arguments = [
    ParseArgument(
        'station',
        help='Specify the full name of the station (SYS NAME/STN NAME is also supported).',
        type=str,
        nargs='+',
    ),
]
switches = [
    MutuallyExclusiveGroup(
        ParseArgument(
            '--update', '-u',
            help='Indicates you expect the entry to already exist.',
            action='store_true',
        ),
        ParseArgument(
            '--remove', '-rm',
            help='Indicates you want to remove an entry.',
            action='store_true',
        ),
        ParseArgument(
            '--add', '-a',
            help='Indicates you want to add a new station.',
            action='store_true',
        ),
    ),
    ParseArgument(
        '--ls-from-star',
        help='Number of light seconds between station and star.',
        type=int,
        dest='lsFromStar',
    ),
    # Note: these are not the usual arguments, they're asking the
    # user to assign rather than select values.
    ParseArgument(
        '--black-market', '--bm',
        help='Does the station have a black market (Y or N) or ? if unknown.',
        choices=['Y', 'y', 'N', 'n', '?'],
        dest='blackMarket',
    ),
    ParseArgument(
        '--market',
        help='Does the station have a commodities market (Y or N), ? for unknown.',
        choices=['Y', 'y', 'N', 'n', '?'],
    ),
    ParseArgument(
        '--shipyard',
        help='Does the station have a shipyard (Y or N) or ? if unknown.',
        choices=['Y', 'y', 'N', 'n', '?'],
    ),
    ParseArgument(
        '--pad-size',
        help='Maximum supported pad size (S, M, or L) or ? if unknown.',
        choices=['S', 's', 'M', 'm', 'L', 'l', '?'],
        dest='padSize',
    ),
    ParseArgument(
        '--outfitting',
        help='Does the station provide outfitting (Y or N) or ? if unknown.',
        choices=['Y', 'y', 'N', 'n', '?'],
    ),
    ParseArgument(
        '--rearm', '--arm',
        help='Does the station provide rearming (Y or N) or ? if unknown.',
        choices=['Y', 'y', 'N', 'n', '?'],
    ),
    ParseArgument(
        '--refuel',
        help='Does the station provide refueling (Y or N) or ? if unknown.',
        choices=['Y', 'y', 'N', 'n', '?'],
    ),
    ParseArgument(
        '--repair',
        help='Does the station provide repairs (Y or N) or ? if unknown.',
        choices=['Y', 'y', 'N', 'n', '?'],
    ),
    ParseArgument(
        '--planetary',
        help='Is the station on a planet (Y or N) or ? if unknown.',
        choices=['Y', 'y', 'N', 'n', '?'],
    ),
    ParseArgument(
        '--confirm',
        help='For confirmation suspicious looking station names.',
        metavar='CONFIRMATION CODE',
        type=str,
    ),
    ParseArgument(
        '--no-export',
        help='Do not update the .csv files.',
        action='store_true',
        dest='noExport',
    )
]

######################################################################
# Helpers

def makeConfirmationCode(base, candidates):
    """
    Makes a four-digit hex checksum of a station list.
    """
    checksum = base
    for cand in candidates:
        for character in cand:
            checksum <<= 4
            checksum += ord(character)
    
    # python integers don't overflow, so we only need
    # to modulo at the end of the checksum.
    checksum %= 65521       # arbitrary prime < 2^32
    
    return hex(checksum).upper()[2:]


def checkStationDoesNotExist(tdb, cmdenv, system, stationName):
    if not system.stations:
        return
    
    upperName = stationName.upper()
    similarities = set()
    try:
        station = tdb.lookupStation(stationName, system)
        if station.dbname.upper() == upperName:
            raise CommandLineError(
                    "Station \"{}\" "
                    "in system \"{}\" "
                    "already exists.".format(
                        stationName, system.name(),
            ))
        similarities.add(station.dbname.upper())
    except LookupError:
        pass
    except AmbiguityError as e:
        for cand in e.anyMatch:
            similarities.add(e.key(cand).upper())
    
    # Check to see if there are stations with somewhat
    # similar names, but allow the user to get around
    # cases where difflib matches 'X Port' to 'Y Port'.
    stationNames = [
        stn.dbname.upper()
        for stn in system.stations
    ]
    cmdenv.DEBUG0("Comparing {} to {}".format(
            upperName, list(stationNames),
    ))
    candidates = difflib.get_close_matches(
            upperName, stationNames, cutoff=0.6,
    )
    for cand in candidates:
        similarities.add(cand)
    
    if not similarities:
        return
    
    confCode = makeConfirmationCode(system.ID, similarities)
    
    if not cmdenv.confirm:
        raise CommandLineError(
                "\"{}\" contains similar station names:\n"
                "  {}\n"
                "\n"
                "If you want to add this station anyway, re-run the "
                "command and add:\n"
                "  --conf {}".format(
                    system.name(),
                    ', '.join(candidates),
                    confCode
        ))
    
    if cmdenv.confirm.upper() != confCode:
        raise CommandLineError(
            "Wrong confirmation code."
        )
    
    cmdenv.NOTE("Confirmation code accepted.")


def checkSystemAndStation(tdb, cmdenv):
    # In add mode, the user has to be more specific.
    stnName = ' '.join(cmdenv.station).strip()
    
    if not cmdenv.add:
        try:
            station = tdb.lookupPlace(stnName)
        except LookupError:
            raise CommandLineError("Unrecognized Station: {}".format(
                cmdenv.station
            ))
        if not isinstance(station, Station):
            raise CommandLineError(
                "Expecting a STATION, got {}".format(stnName)
            )
        cmdenv.system = station.system.name()
        cmdenv.station = station.dbname
        
        return station.system, station
    
    # Clean up the station name and potentially lift the system
    # name out of it.
    stnName = re.sub(r" +", " ", stnName)
    stnName = re.sub(r"[ /]*/[ /]*", "/", stnName)
    while stnName.startswith('/'):
        stnName = stnName[1:]
    slashPos = stnName.find('/')
    if slashPos > 0:
        sysName, stnName = stnName[:slashPos], stnName[slashPos+1:]
        sysName = sysName.upper()
    else:
        sysName = None
    
    if not stnName:
        raise CommandLineError("Invalid station name: {}".format(
                envStnName
        ))
    
    if not sysName:
        raise CommandLineError("No system name specified")
    
    cmdenv.system, cmdenv.station = sysName, utils.titleFixup(stnName)
    try:
        system = tdb.lookupSystem(sysName)
    except LookupError:
        raise CommandLineError(
                "Unknown SYSTEM name: \"{}\"".format(
                    sysName
        ))
    
    # check the station does not exist
    checkStationDoesNotExist(tdb, cmdenv, system, stnName)
    
    return system, None


def addStation(tdb, cmdenv, system, stationName):
    return tdb.addLocalStation(
            system=system,
            name=stationName,
            lsFromStar=cmdenv.lsFromStar or 0,
            market=cmdenv.market or '?',
            blackMarket=cmdenv.blackMarket or '?',
            shipyard=cmdenv.shipyard or '?',
            outfitting=cmdenv.outfitting or '?',
            rearm=cmdenv.rearm or '?',
            refuel=cmdenv.refuel or '?',
            repair=cmdenv.repair or '?',
            maxPadSize=cmdenv.padSize or '?',
            planetary=cmdenv.planetary or '?',
            commit=True,
    )


def updateStation(tdb, cmdenv, station):
    return tdb.updateLocalStation(
            station=station,
            lsFromStar=cmdenv.lsFromStar,
            market=cmdenv.market,
            blackMarket=cmdenv.blackMarket,
            shipyard=cmdenv.shipyard,
            outfitting=cmdenv.outfitting,
            rearm=cmdenv.rearm,
            refuel=cmdenv.refuel,
            repair=cmdenv.repair,
            maxPadSize=cmdenv.padSize,
            planetary=cmdenv.planetary,
            force=True,
            commit=True,
    )


def removeStation(tdb, cmdenv, station):
    db = tdb.getDB()
    db.execute("""
            DELETE FROM Station WHERE station_id = ?
    """, [station.ID])
    db.commit()
    cmdenv.NOTE("{} (#{}) removed from {} database.",
            station.name(), station.ID, tdb.dbPath)
    cmdenv.stationItemCount = station.itemCount
    return True


def checkResultAndExportStations(tdb, cmdenv, result):
    if not result:
        cmdenv.NOTE("No changes.")
        return None
    if cmdenv.noExport:
        cmdenv.DEBUG0("no-export set, not exporting stations")
        return None
    
    lines, csvPath = csvexport.exportTableToFile(tdb, cmdenv, "Station")
    cmdenv.NOTE("{} updated.", csvPath)
    
    if cmdenv.remove:
        if cmdenv.stationItemCount:
            cmdenv.NOTE("Station had items, regenerating .prices file")
            cache.regeneratePricesFile(tdb, cmdenv)
    
    return None


######################################################################
# Perform query and populate result set

def run(results, cmdenv, tdb):
    if cmdenv.lsFromStar and cmdenv.lsFromStar < 0:
        raise CommandLineError("Invalid (negative) --ls option")
    
    system, station = checkSystemAndStation(tdb, cmdenv)
    
    systemName = cmdenv.system
    stationName = cmdenv.station
    
    if cmdenv.add:
        result = addStation(tdb, cmdenv, system, stationName)
        return checkResultAndExportStations(tdb, cmdenv, result)
    elif cmdenv.update:
        result = updateStation(tdb, cmdenv, station)
        return checkResultAndExportStations(tdb, cmdenv, result)
    elif cmdenv.remove:
        result = removeStation(tdb, cmdenv, station)
        return checkResultAndExportStations(tdb, cmdenv, result)
    
    # Otherwise, it's just a query
    results.summary = ResultRow()
    results.summary.system = station.system
    results.summary.station = station
    
    avgSell = results.summary.avgSelling = tdb.getAverageSelling()
    avgBuy = results.summary.avgBuying = tdb.getAverageBuying()
    
    class ItemTrade(object):
        def __init__(self, ID, price, avgAgainst):
            self.ID, self.item = ID, tdb.itemByID[ID]
            self.price = int(price)
            self.avgTrade = avgAgainst.get(ID, 0)
    
    # Look up all selling and buying by the station
    selling, buying = [], []
    cur = tdb.query("""
        SELECT  item_id, demand_price, supply_price
          FROM  StationItem
         WHERE  station_id = ?
                AND (demand_price > 10 or supply_price > 10)
    """, [station.ID])
    for ID, demand_price, supply_price in cur:
        if demand_price > 10 and avgSell.get(ID, 0) > 10:
            buying.append(ItemTrade(ID, demand_price, avgSell))
        if supply_price > 10 and avgBuy.get(ID, 0) > 10:
            selling.append(ItemTrade(ID, supply_price, avgBuy))
    selling.sort(
            key=lambda item: item.price - item.avgTrade,
    )
    results.summary.selling = selling[:5]
    buying.sort(
            key=lambda item: item.avgTrade - item.price,
    )
    results.summary.buying = buying[:5]
    
    return results

def render(results, cmdenv, tdb):
    system, station = results.summary.system, results.summary.station
    
    if cmdenv.detail:
        sysDetail = "(#{} @ {},{},{})".format(
            system.ID, system.posX, system.posY, system.posZ
        )
    else:
        sysDetail = "(#{})".format(system.ID)
    
    print("Station Data:")
    print("System....:", system.name(), sysDetail)
    print("Station...:", station.dbname, "(#{})".format(station.ID))
    
    if cmdenv.detail:
        siblings = ", ".join(
            stn.dbname
            for stn in system.stations
            if stn is not station
        )
        if siblings:
            print("Also Here.:", siblings)
    
    ls = station.distFromStar()
    if cmdenv.detail and ls == '?':
        ls = '0 [unknown]'
    print("Stn/Ls....:", ls)
    
    def _detail(value, source):
        detail = source[value]
        if cmdenv.detail and detail == '?':
            detail += ' [unknown]'
        return detail
    print("Pad Size..:", _detail(station.maxPadSize, TradeDB.padSizes))
    print("Market....:", _detail(station.market, TradeDB.marketStates))
    print("B/Market..:", _detail(station.blackMarket, TradeDB.marketStates))
    print("Shipyard..:", _detail(station.shipyard, TradeDB.marketStates))
    print("Outfitting:", _detail(station.outfitting, TradeDB.marketStates))
    print("Rearm.....:", _detail(station.rearm, TradeDB.marketStates))
    print("Refuel....:", _detail(station.refuel, TradeDB.marketStates))
    print("Repair....:", _detail(station.repair, TradeDB.marketStates))
    print("Planetary.:", _detail(station.planetary, TradeDB.planetStates))
    print("Prices....:", station.itemCount or 'None')
    
    if station.itemCount == 0:
        return
    
    newest, oldest = tdb.query("""
            SELECT JULIANDAY('NOW') - JULIANDAY(MAX(si.modified)),
                   JULIANDAY('NOW') - JULIANDAY(MIN(si.modified))
              FROM StationItem si
             WHERE station_id = ?
    """, [station.ID]).fetchone()
    if newest or oldest:
        # less than a quarter hour difference? ignore?
        if abs(newest - oldest) < (1 / (24 * 4)):
            pricesAge = "{:.2f} days".format(oldest)
        else:
            pricesAge = "{:.2f}-{:.2f} days".format(newest, oldest)
    else:
        pricesAge = "[n/a]"
    
    print("Price Age.:", pricesAge)
    
    def makeBest(rows, explanation, alt, maxLen, starFn):
        if not rows:
            return "[n/a]"
        best = []
        for irow in rows:
            star = '*' if starFn(irow.price, irow.avgTrade) else ''
            best.append([irow, star])
        
        if not cmdenv.detail:
            return ', '.join(irow[0].item.name() + irow[1] for irow in best)
        
        bestText = "("+explanation+")"
        for irow in best:
            bestText += "\n    {:<{len}} @ {:7n}cr (Avg {} {:7n}cr)".format(
                    irow[0].item.name(cmdenv.detail) + irow[1],
                    irow[0].price,
                    alt,
                    irow[0].avgTrade,
                    len=maxLen + 1,
            )
        return bestText


    longestNameLen = max(
        max_len(results.summary.selling, key=lambda row: row.item.name(cmdenv.detail)),
        max_len(results.summary.buying, key=lambda row: row.item.name(cmdenv.detail)),
    )
    print("Best Buy..:", makeBest(
            results.summary.selling, "Buy from this station", "Sell", longestNameLen,
            starFn=lambda price, avgCr: \
                price <= (avgCr * 0.9),
    ))
    print("Best Sale.:", makeBest(
            results.summary.buying, "Sell to this station", "Cost", longestNameLen,
            starFn=lambda price, avgCr: \
                price >= (avgCr * 1.1),
    ))